<?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>Deploying PNETLab on GCP With Terraform and Tailscale on Ef&#39;s log</title>
    <link>https://fahmifj.github.io/series/deploying-pnetlab-on-gcp-with-terraform-and-tailscale/</link>
    <description>Recent content in Deploying PNETLab on GCP With Terraform and Tailscale on Ef&#39;s log</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <lastBuildDate>Wed, 26 Mar 2025 13:27:29 +0700</lastBuildDate><atom:link href="https://fahmifj.github.io/series/deploying-pnetlab-on-gcp-with-terraform-and-tailscale/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>PNETLab on Google Cloud – Part 1: Server Deployment</title>
      <link>https://fahmifj.github.io/blog/deploy-pnetlab-on-gcp-part-1/</link>
      <pubDate>Wed, 26 Mar 2025 13:27:29 +0700</pubDate>
      
      <guid>https://fahmifj.github.io/blog/deploy-pnetlab-on-gcp-part-1/</guid>
      <description>Introduction While running GNS3 inside Orbstack, I encountered an issue with Orbstack&amp;rsquo;s networking system that prevented my localhost from communicating with GNS3 appliances. Instead of spending more time troubleshooting something that is limited by its design, I decided to deploy a cloud-hosted network lab using PNETLab.
The lab infrastructure is provisioned using Terraform and remote access to the lab is secured through Tailscale instead of exposing it directly to the Internet.</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>While running <a href="/articles/running-gns3-on-apple-m2-orbstack/"target="_blank" rel="noopener noreferrer"
>GNS3 inside Orbstack</a>, I encountered an issue with Orbstack&rsquo;s networking system that prevented my localhost from communicating with GNS3 appliances. Instead of spending more time troubleshooting something that is limited by its design, I decided to deploy a cloud-hosted network lab using PNETLab.</p>
<p>The lab infrastructure is provisioned using Terraform and remote access to the lab is secured through Tailscale instead of exposing it directly to the Internet.</p>
<p>This article covers the deployment process. The remote access setup will be covered in a separate article.</p>
<h3 id="goals">Goals</h3>
<ul>
<li>Build reusable PNETLab server with Terraform.</li>
</ul>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>GCP &amp; Tailscale accounts.</li>
<li>GCP CLI installed.</li>
<li>Terraform installed.</li>
</ul>
<h2 id="architecture-overview"><strong>Architecture Overview</strong></h2>
<p>Here’s an overview of what we’re going to build:</p>
<img src="./imgs/image-20251028001404879.png" alt="image-20251028001404879" style="zoom:50%;" />
<p>The setup consists of:</p>
<ul>
<li><strong>Terraform for infrastructure automation</strong>.</li>
<li><strong>A GCP compute instance running PNETLab</strong>.</li>
<li><strong>A private VPC network</strong>.</li>
<li><strong>Cloud Router for outbound internet access (dynamic IP)</strong>.</li>
<li>Tailscale for secure remote access to PNETLab instance</li>
</ul>
<h2 id="prepare-the-gcp-environment">Prepare the GCP Environment</h2>
<p>Before we begin, several prerequisites need to be prepared within Google Cloud Platform.</p>
<blockquote>
<p>Note: I&rsquo;m using GCP&rsquo;s free trial credits for this setup, if you&rsquo;re on a paid account, the steps remain the same.</p>
</blockquote>
<h3 id="gcp-authentication">GCP Authentication</h3>
<p>Terraforms requires credentials to access your Google Cloud resources. There are several methods for this but I will be using Application Default Credentials (ADC) method for this personal project.</p>
<p>From CLI, run the following:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ gcloud auth application-default login</span></span></code></pre>
</figure><p>A browser window will pop ups, requesting for Google account authentication. After a successful authtentication, the credentials will be stored in the default location and Terraform will look into it automatically.</p>
<h3 id="creating-a-gcp-project">Creating a GCP Project</h3>
<p>First, create a new project for our PNETLab deployment using GCP CLI.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ <span class="nv">PROJECT_ID</span><span class="o">=</span>pnetlab-<span class="nv">$RANDOM</span>
</span></span><span class="line"><span class="cl">$ <span class="nb">echo</span> <span class="nv">$PROJECT_ID</span>
</span></span><span class="line"><span class="cl">pnetlab-xxx
</span></span><span class="line"><span class="cl">$ mkdir <span class="nv">$PROJECT_ID</span>
</span></span><span class="line"><span class="cl">$ <span class="nb">cd</span> <span class="nv">$PROJECT_ID</span>
</span></span><span class="line"><span class="cl">$ gcloud projects create <span class="nv">$PROJECT_ID</span>
</span></span><span class="line"><span class="cl">$ gcloud config <span class="nb">set</span> project <span class="nv">$PROJECT_ID</span></span></span></code></pre>
</figure><p>Then, link the project to your <a href="https://cloud.google.com/billing/docs/how-to/create-billing-account"target="_blank" rel="noopener noreferrer"
>billing account</a>.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ gcloud billing accounts list              
</span></span><span class="line"><span class="cl">ACCOUNT_ID            NAME                OPEN  MASTER_ACCOUNT_ID
</span></span><span class="line"><span class="cl">AAAAAA-BBBBBB-CCCCCC  My Billing Account  True
</span></span><span class="line"><span class="cl">$ gcloud billing projects link <span class="nv">$PROJECT_ID</span> --billing-account<span class="o">=</span>AAAAAA-BBBBBB-CCCCCC</span></span></code></pre>
</figure><h3 id="enabling-compute-engine-api">Enabling Compute Engine API</h3>
<p>To allow Terraform to manage VM resources, we must enable the Compute Engine API.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ gcloud services <span class="nb">enable</span> compute.googleapis.com</span></span></code></pre>
</figure><h2 id="server-deployment-with-terraform">Server Deployment with Terraform</h2>
<p>We will use Terraform to provision our server that will host PNETLab.</p>
<h3 id="preparing-project-structure">Preparing Project Structure</h3>
<p>We&rsquo;ll use the following folder structure.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ tree
</span></span><span class="line"><span class="cl">.
</span></span><span class="line"><span class="cl">├── README.MD
</span></span><span class="line"><span class="cl">└── terraform
</span></span><span class="line"><span class="cl">    ├── terraforms.tfvars <span class="c1"># sensitive informations goes here (project_id, region, user/keys)</span>
</span></span><span class="line"><span class="cl">    ├── main.tf <span class="c1"># defined gcp resources</span>
</span></span><span class="line"><span class="cl">    └── variables.tf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="m">1</span> directories, <span class="m">4</span> files</span></span></code></pre>
</figure><h4 id="terraformtfvars">terraform.tfvars</h4>
<p><code>terraforms.tfvars</code> contains the following:</p>
<figure class="highlight">
    <pre tabindex="0"><code class="language-" data-lang="">project_id      = &#34;&#34;
region          = &#34;&#34;
zone            = &#34;&#34;
os_image        = &#34;projects/ubuntu-os-cloud/global/images/ubuntu-1804-bionic-v20240116a&#34;</code></pre>
</figure><p>Other than <code>os_image</code>, make your adjustment.</p>
<h4 id="maintf">main.tf</h4>
<p><code>main.tf</code> file contains the following:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">terraform</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">required_providers</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">google</span> <span class="err">=</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">      <span class="err">source</span> <span class="err">=</span> <span class="nt">&#34;hashicorp/google&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="err">version</span> <span class="err">=</span> <span class="s2">&#34;~&gt; 4.5&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="err">}</span>
</span></span><span class="line"><span class="cl"><span class="err">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">provider</span> <span class="s2">&#34;google&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">project</span> <span class="err">=</span> <span class="err">var.project_id</span>
</span></span><span class="line"><span class="cl">  <span class="err">region</span>  <span class="err">=</span> <span class="err">var.region</span>
</span></span><span class="line"><span class="cl">  <span class="err">zone</span>    <span class="err">=</span> <span class="err">var.zone</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">#</span> <span class="err">SETUP</span> <span class="err">NETWORK</span> <span class="err">#</span>
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_network&#34;</span> <span class="s2">&#34;pnetlab_vpc&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span>                    <span class="err">=</span> <span class="nt">&#34;pnetlab-vpc&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">auto_create_subnetworks</span> <span class="err">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_subnetwork&#34;</span> <span class="s2">&#34;pnetlab_subnet&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span>          <span class="err">=</span> <span class="nt">&#34;pnetlab-subnet&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">network</span>       <span class="err">=</span> <span class="err">google_compute_network.pnetlab_vpc.id</span>
</span></span><span class="line"><span class="cl">  <span class="err">ip_cidr_range</span> <span class="err">=</span> <span class="s2">&#34;10.10.1.0/24&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_firewall&#34;</span> <span class="s2">&#34;allow_ssh_access&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span>    <span class="err">=</span> <span class="nt">&#34;allow-ssh&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">network</span> <span class="err">=</span> <span class="err">google_compute_network.pnetlab_vpc.id</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">allow</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">protocol</span> <span class="err">=</span> <span class="nt">&#34;tcp&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="err">ports</span>    <span class="err">=</span> <span class="p">[</span><span class="s2">&#34;22&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">source_ranges</span> <span class="err">=</span> <span class="p">[</span> <span class="s2">&#34;0.0.0.0/0&#34;</span> <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">#</span> <span class="err">NAT</span>
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_router&#34;</span> <span class="s2">&#34;pnetlab_router&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span> <span class="err">=</span> <span class="nt">&#34;pnetlab-router&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">region</span> <span class="err">=</span> <span class="err">var.region</span>
</span></span><span class="line"><span class="cl">  <span class="err">network</span> <span class="err">=</span> <span class="err">google_compute_network.pnetlab_vpc.id</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_router_nat&#34;</span> <span class="s2">&#34;pnetlab_router_nat&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span> <span class="err">=</span> <span class="nt">&#34;pnetlab-router-nat&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">region</span> <span class="err">=</span> <span class="err">var.region</span>
</span></span><span class="line"><span class="cl">  <span class="err">router</span> <span class="err">=</span> <span class="err">google_compute_router.pnetlab_router.name</span>
</span></span><span class="line"><span class="cl">  <span class="err">nat_ip_allocate_option</span> <span class="err">=</span> <span class="s2">&#34;AUTO_ONLY&#34;</span> <span class="err">#</span> <span class="err">use</span> <span class="err">dynamic</span> <span class="err">public</span> <span class="err">ip</span> <span class="err">for</span> <span class="err">nat</span>
</span></span><span class="line"><span class="cl">  <span class="err">source_subnetwork_ip_ranges_to_nat</span> <span class="err">=</span> <span class="s2">&#34;ALL_SUBNETWORKS_ALL_IP_RANGES&#34;</span> <span class="err">#</span> <span class="err">nat</span> <span class="err">any</span> <span class="err">sources</span> <span class="err">ip</span> <span class="err">range</span> <span class="err">available</span> <span class="err">within</span> <span class="err">the</span> <span class="err">vpc</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">#</span> <span class="err">SETUP</span> <span class="err">EXT</span> <span class="err">DISK</span> <span class="err">#</span>
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_disk&#34;</span> <span class="s2">&#34;additional_disk&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span>   <span class="err">=</span> <span class="nt">&#34;pnetlab-additional-disk&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">type</span>   <span class="err">=</span> <span class="s2">&#34;pd-standard&#34;</span>  <span class="err">#</span> <span class="err">Choose</span> <span class="err">disk</span> <span class="err">type</span> <span class="err">(pd-standard</span><span class="p">,</span> <span class="err">pd-ssd,</span> <span class="err">etc.)</span>
</span></span><span class="line"><span class="cl">  <span class="err">size</span> <span class="err">=</span> <span class="err">50</span> 
</span></span><span class="line"><span class="cl">  <span class="err">lifecycle</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">prevent_destroy</span> <span class="err">=</span> <span class="err">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="err">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">#</span> <span class="err">SETUP</span> <span class="err">INSTANCES</span> <span class="err">#</span>
</span></span><span class="line"><span class="cl"><span class="err">#</span> <span class="err">ubuntu</span><span class="mi">-1804</span><span class="err">-bionic-v</span><span class="mi">20240116</span><span class="err">a</span>                                 <span class="err">ubuntu-os-cloud</span>      <span class="err">ubuntu</span><span class="mi">-1804</span><span class="err">-lts</span>
</span></span><span class="line"><span class="cl"><span class="err">resource</span> <span class="s2">&#34;google_compute_instance&#34;</span> <span class="s2">&#34;pnetlab_server&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">name</span>         <span class="err">=</span> <span class="nt">&#34;pnetlab-server&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">machine_type</span> <span class="err">=</span> <span class="s2">&#34;n2-standard-2&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">tags</span>         <span class="err">=</span> <span class="p">[</span><span class="s2">&#34;pnetlab&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">boot_disk</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">initialize_params</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">      <span class="err">image</span> <span class="err">=</span> <span class="err">var.os_image</span> <span class="err">#</span> <span class="err">cloud</span> <span class="err">image</span>
</span></span><span class="line"><span class="cl">      <span class="err">size</span>  <span class="err">=</span> <span class="err">20</span>                 <span class="err">#</span> <span class="err">assign</span> <span class="err">20gb</span>
</span></span><span class="line"><span class="cl">      <span class="err">type</span>  <span class="err">=</span> <span class="nt">&#34;pd-standard&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="err">#</span> <span class="err">external</span> <span class="err">disk</span> <span class="mi">50</span><span class="err">gb</span>
</span></span><span class="line"><span class="cl">  <span class="err">attached_disk</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">source</span> <span class="err">=</span> <span class="err">google_compute_disk.additional_disk.id</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">network_interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">network</span>    <span class="err">=</span> <span class="err">google_compute_network.pnetlab_vpc.id</span>
</span></span><span class="line"><span class="cl">    <span class="err">subnetwork</span> <span class="err">=</span> <span class="err">google_compute_subnetwork.pnetlab_subnet.id</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="err">#</span> <span class="err">Disable</span> <span class="err">public</span> <span class="err">IP</span>
</span></span><span class="line"><span class="cl">    <span class="err">#</span> <span class="err">access_config</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">#</span>  <span class="err">network_tier</span> <span class="err">=</span> <span class="nt">&#34;STANDARD&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="err">#</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="err">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">scheduling</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">automatic_restart</span>           <span class="err">=</span> <span class="err">true</span>
</span></span><span class="line"><span class="cl">    <span class="err">on_host_maintenance</span>         <span class="err">=</span> <span class="nt">&#34;MIGRATE&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="err">preemptible</span>                 <span class="err">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">    <span class="err">provisioning_model</span>          <span class="err">=</span> <span class="s2">&#34;STANDARD&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="err">advanced_machine_features</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">enable_nested_virtualization</span> <span class="err">=</span> <span class="err">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="err">}</span></span></span></code></pre>
</figure><p>This configuration provisions:</p>
<ul>
<li>Custom VPC network.</li>
<li>Subnet.</li>
<li>Firewall rules.</li>
<li>Cloud NAT router.</li>
<li>Additional persistent disk.</li>
<li>Enabled nested virtualization (need machine type that support nested virtualization).</li>
</ul>
<p>There&rsquo;s also n1 for alternative machine type if you worry about the cost, lets say <code>n1-custom-1-2048</code>, which means machine type is <code>n1</code> with single core and  2 GB of RAM.</p>
<h4 id="variablestf">variables.tf</h4>
<p><code>variables.tf</code> define the variables we used in the <code>tfvars</code> file, it contains:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">variable</span> <span class="s2">&#34;project_id&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">type</span>        <span class="err">=</span> <span class="err">string</span>
</span></span><span class="line"><span class="cl">  <span class="err">description</span> <span class="err">=</span> <span class="nt">&#34;Project ID&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">variable</span> <span class="s2">&#34;region&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">type</span>        <span class="err">=</span> <span class="err">string</span>
</span></span><span class="line"><span class="cl">  <span class="err">description</span> <span class="err">=</span> <span class="nt">&#34;Project region&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">default</span>     <span class="err">=</span> <span class="s2">&#34;us-central&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">variable</span> <span class="s2">&#34;zone&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">type</span>        <span class="err">=</span> <span class="err">string</span>
</span></span><span class="line"><span class="cl">  <span class="err">description</span> <span class="err">=</span> <span class="nt">&#34;Project zone&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="err">default</span>     <span class="err">=</span> <span class="s2">&#34;us-central1-a&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">variable</span> <span class="s2">&#34;os_image&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">type</span>        <span class="err">=</span> <span class="err">string</span>
</span></span><span class="line"><span class="cl">  <span class="err">description</span> <span class="err">=</span> <span class="nt">&#34;OS family&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</figure><h3 id="vm-deployment">VM Deployment</h3>
<p>Within the terraform folder, initialize the terraform working directory.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ terraform init</span></span></code></pre>
</figure><p>The init step only needs to be performed be performed during the first deployment or after provider/module changes.</p>
<p>With all the resource files defined, we can start the deployment.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ terraform fmt         <span class="c1"># Format your Terraform files  </span>
</span></span><span class="line"><span class="cl">$ terraform validate    <span class="c1"># Check for syntax errors  </span>
</span></span><span class="line"><span class="cl">$ terraform plan -out pnetlab-deploy       <span class="c1">#Preview the infrastructure changes  </span>
</span></span><span class="line"><span class="cl">$ terraform apply pnetlab-deploy  <span class="c1"># Deploy the resources  </span></span></span></code></pre>
</figure><h3 id="verify-deployment">Verify Deployment</h3>
<p>After it finishes, check if the instance is running:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ gcloud compute instances list --filter<span class="o">=</span><span class="s2">&#34;name=pnetlab-server&#34;</span> --project<span class="o">=</span><span class="s2">&#34;</span><span class="nv">$PROJECT_ID</span><span class="s2">&#34;</span></span></span></code></pre>
</figure><blockquote>
<p>Please mind that the terraform config I provide here is only reusable for personal project, it&rsquo;s not reusable for team scale project.</p>
</blockquote>
<h2 id="pnetlab-installation">PNETLab Installation</h2>
<h3 id="install-pnetlab">Install PNETLab</h3>
<p>SSH into the instance using <code>gcloud</code> cli.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ gcloud compute ssh INSTANCE_NAME --zone<span class="o">=</span>YOUR_ZONE</span></span></code></pre>
</figure><p>Add pnetlab repository with the following command.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ <span class="nb">echo</span> <span class="s2">&#34;deb [trusted=yes] http://repo.pnetlab.com ./&#34;</span> <span class="p">|</span> sudo tee -a /etc/apt/sources.list</span></span></code></pre>
</figure><p>Update the repository and install pnetlab.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo apt-get update
</span></span><span class="line"><span class="cl">$ sudo apt-get install pnetlab</span></span></code></pre>
</figure><p>During installation process, you might be asked to replace kernel configuration like this</p>
<p><div class="img-container"><img src="./imgs/image-20260528204224869.png" alt="image-20260528204224869"  /></div>
</p>
<p>Just go with it and continue the installation.</p>
<p>Once it finished, you will have a name resolution/DNS issue, but that&rsquo;s fine for now.</p>
<h3 id="server-configuration">Server Configuration</h3>
<h4 id="swap">Swap</h4>
<p>Create a swap disk, allocate the size according to your need.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo fallocate -l 1G /swapfile
</span></span><span class="line"><span class="cl">$ sudo chmod <span class="m">600</span> /swapfile
</span></span><span class="line"><span class="cl">$ sudo mkswap /swapfile
</span></span><span class="line"><span class="cl">$ sudo swapon /swapfile
</span></span><span class="line"><span class="cl">$ sudo cp /etc/fstab<span class="o">{</span>,.bak<span class="o">}</span>
</span></span><span class="line"><span class="cl">$ sudo <span class="s1">&#39;/swapfile none swap sw 0 0&#39;</span> <span class="p">|</span> sudo tee -a /etc/fstab</span></span></code></pre>
</figure><h4 id="dns">DNS</h4>
<p>Edit file <code>/etc/network/interfaces</code> add line <code>dns-nameservers 8.8.8.8</code> to the configuration of interface <code>pnet0</code>.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">...</span>
</span></span><span class="line"><span class="cl"><span class="c"># The primary network interface</span>
</span></span><span class="line"><span class="cl"><span class="nx">iface</span> <span class="nx">eth0</span> <span class="nx">inet</span> <span class="nx">manual</span>
</span></span><span class="line"><span class="cl"><span class="nx">auto</span> <span class="nx">pnet0</span>
</span></span><span class="line"><span class="cl"><span class="nx">iface</span> <span class="nx">pnet0</span> <span class="nx">inet</span> <span class="nx">dhcp</span>
</span></span><span class="line"><span class="cl">    <span class="nx">dns-nameservers</span> <span class="mf">8.8</span><span class="p">.</span><span class="mf">8.8</span>	
</span></span><span class="line"><span class="cl">    <span class="nx">bridge_ports</span> <span class="nx">eth0</span>
</span></span><span class="line"><span class="cl">    <span class="nx">bridge_stp</span> <span class="nx">off</span>
</span></span><span class="line"><span class="cl"><span class="p">...</span></span></span></code></pre>
</figure><p>And restart the networking service.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo service networking restart</span></span></code></pre>
</figure><h4 id="disk">Disk</h4>
<p>Time to add the additional disk we defined in the terraform resource. The purpose of this disk is to store all the pnetlab project files as well as the images later. We can preserve the lab data in this disk in case the VM need to be replaced/gone wrong, we will get into the detail later.</p>
<p>In my case, the additional disk is available at <code>/dev/sdb</code>.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo lsblk
</span></span><span class="line"><span class="cl">NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
</span></span><span class="line"><span class="cl">loop0     7:0    <span class="m">0</span> 40.4M  <span class="m">1</span> loop /snap/snapd/20671
</span></span><span class="line"><span class="cl">loop1     7:1    <span class="m">0</span>  368M  <span class="m">1</span> loop /snap/google-cloud-cli/203
</span></span><span class="line"><span class="cl">loop2     7:2    <span class="m">0</span> 63.9M  <span class="m">1</span> loop /snap/core20/2105
</span></span><span class="line"><span class="cl">sda       8:0    <span class="m">0</span>   20G  <span class="m">0</span> disk 
</span></span><span class="line"><span class="cl">├─sda1    8:1    <span class="m">0</span> 19.9G  <span class="m">0</span> part /
</span></span><span class="line"><span class="cl">├─sda14   8:14   <span class="m">0</span>    4M  <span class="m">0</span> part 
</span></span><span class="line"><span class="cl">└─sda15   8:15   <span class="m">0</span>  106M  <span class="m">0</span> part /boot/efi
</span></span><span class="line"><span class="cl">sdb       8:16   <span class="m">0</span>  100G  <span class="m">0</span> disk </span></span></code></pre>
</figure><p>We&rsquo;ll create an LVM disk out of it.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo vgcreate /dev/vg01 /dev/sdb
</span></span><span class="line"><span class="cl">  Volume group <span class="s2">&#34;vg01&#34;</span> successfully created
</span></span><span class="line"><span class="cl">$ sudo lvcreate -l 100%FREE --name lv01 vg01 
</span></span><span class="line"><span class="cl">  Logical volume <span class="s2">&#34;lv01&#34;</span> created.
</span></span><span class="line"><span class="cl">$ sudo mkfs.ext4 /dev/vg01/lv01 
</span></span><span class="line"><span class="cl">mke2fs 1.44.1 <span class="o">(</span>24-Mar-2018<span class="o">)</span>
</span></span><span class="line"><span class="cl">Discarding device blocks: <span class="k">done</span>                            
</span></span><span class="line"><span class="cl">Creating filesystem with <span class="m">26213376</span> 4k blocks and <span class="m">6553600</span> inodes
</span></span><span class="line"><span class="cl">Filesystem UUID: f1173542-f07c-493f-89e6-188439880465
</span></span><span class="line"><span class="cl">Superblock backups stored on blocks: 
</span></span><span class="line"><span class="cl">  32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
</span></span><span class="line"><span class="cl">  4096000, 7962624, 11239424, 20480000, <span class="m">23887872</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Allocating group tables: <span class="k">done</span>                            
</span></span><span class="line"><span class="cl">Writing inode tables: <span class="k">done</span>                            
</span></span><span class="line"><span class="cl">Creating journal <span class="o">(</span><span class="m">131072</span> blocks<span class="o">)</span>: <span class="k">done</span>
</span></span><span class="line"><span class="cl">Writing superblocks and filesystem accounting information: <span class="k">done</span></span></span></code></pre>
</figure><p>Then we set a mount point for the volume we just created at <code>/mnt/unetlab</code></p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo mkdir /mnt/unetlab
</span></span><span class="line"><span class="cl">$ sudo mount /dev/vg01/lv01 /mnt/unetlab</span></span></code></pre>
</figure><p>Edit the <code>/etc/fstab</code> file and add the following line to mount <code>/mnt/unetlab</code> on boot:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">/dev/vg01/lv01  /mnt/unetlab  ext4  defaults  0  0</span></span></span></code></pre>
</figure><p>Now we&rsquo;ll create a new directory for PNETLab project files and image files in our newly mounted disk.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo mkdir /mnt/unetlab/addons
</span></span><span class="line"><span class="cl">$ sudo mkdir /mnt/unetlab/labs</span></span></code></pre>
</figure><p>Next, we&rsquo;ll remove the old directories for project files and image files, and replace them with symbolic links pointing to the new directories on the mounted disk.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo rm -rf /opt/unetlab/addons/ 
</span></span><span class="line"><span class="cl">$ sudo rm  -rf /opt/unetlab/labs/
</span></span><span class="line"><span class="cl">$ sudo ln -s /mnt/unetlab/addons/ /opt/unetlab/
</span></span><span class="line"><span class="cl">$ sudo ln -s /mnt/unetlab/labs/ /opt/unetlab/</span></span></code></pre>
</figure><p>Change ownership of the new labs directory to ensure PNETLab&rsquo;s services can access it back.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo chown -R www-data:www-data /mnt/unetlab/labs/</span></span></code></pre>
</figure><h4 id="grub">Grub</h4>
<p>Remove the default grub config.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo rm /etc/default/grub.d/50-cloudimg-settings.cfg
</span></span><span class="line"><span class="cl">$ sudo update-grub</span></span></code></pre>
</figure><h4 id="restart-server">Restart Server</h4>
<p>To complete the installation, restart the server.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ sudo reboot</span></span></code></pre>
</figure><p>Once you logged in, there will be a pop up asking for setting root password, hostname, and NTP. Configure these according to your preferences.</p>
<p>We also need to make sure that the internet connectivity is working with simple <code>ping</code> check to <code>1.1.1.1</code>.</p>
<h3 id="verify-installation">Verify Installation</h3>
<p>Since the server configured without a public IP address, the PNETLab web interface cannot be accessed directly from the internet. The installation can be validated locally using <code>curl</code> from the server.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ curl -k https://localhost</span></span></code></pre>
</figure><p>Expected output:</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">HTTP/1.1 <span class="m">301</span> Moved Permanently
</span></span><span class="line"><span class="cl">Date: Thu, <span class="m">28</span> May <span class="m">2026</span> 15:02:55 GMT
</span></span><span class="line"><span class="cl">Server: Apache/2.4.29 <span class="o">(</span>Ubuntu<span class="o">)</span>
</span></span><span class="line"><span class="cl">Access-Control-Allow-Origin: *
</span></span><span class="line"><span class="cl">Access-Control-Allow-Headers: *
</span></span><span class="line"><span class="cl">Location: /store/public/admin/main/view
</span></span><span class="line"><span class="cl">...</span></span></code></pre>
</figure><h3 id="update-version-optional">Update Version (Optional)</h3>
<p>After the installation completed, we can directly upgrade PNETLab to the latest version. It&rsquo;s an optional, so I will just put the official guide link on how to upgrade the lab version: <a href="https://pnetlab.com/pages/releases"target="_blank" rel="noopener noreferrer"
>go here</a>.</p>
<h2 id="troubleshoot">Troubleshoot</h2>
<p>I know things that may go wrong before you finish it, so there are a few problems I encountered and the solutions.</p>
<h3 id="cannot-install-packages">Cannot install packages</h3>
<p>Try disable/comment this whole url line from the repository.</p>
<figure class="highlight">
    <pre tabindex="0"><code class="language-" data-lang="">#deb [trusted=yes] http://i-share.top/repo ./
#deb [trusted=yes] http://repo.pnetlab.com ./</code></pre>
</figure><h3 id="cloud-appliance-dhcp-not-working">Cloud Appliance: DHCP not working</h3>
<p>Verify <code>udhcpd</code> service is running and enabled.</p>
<figure class="highlight">
    <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ service udhcpd status
</span></span><span class="line"><span class="cl">$ cat /etc/default/udhcpd <span class="p">|</span> grep ENABLED</span></span></code></pre>
</figure><p>Set to enabled and restart to see if it&rsquo;s working.</p>
<h2 id="conclusion">Conclusion</h2>
<p>We have completed the deployment process of PNETLab server on Google Cloud using Terraform. At this stage, the lab is running internally but remains inaccessible from outside the private network.</p>
<p>In the next article, we will configure Tailscale to provide secure remote access to the lab.</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
