<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>networkop on networkop</title>
    <link>https://networkop.co.uk/</link>
    <description>Recent content in networkop on networkop</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <copyright>&amp;copy; Michael Kashin 2021</copyright>
    <lastBuildDate>Sun, 15 Oct 2017 00:00:00 +0000</lastBuildDate>
    <atom:link href="/" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Linux Networking - Source IP address selection</title>
      <link>https://networkop.co.uk/post/2023-09-linux-src/</link>
      <pubDate>Sat, 02 Sep 2023 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2023-09-linux-src/</guid>
      <description>

&lt;p&gt;Any network device, be it a transit router or a host, usually has multiple IP addresses assigned to its interfaces. One of the first things we learn as network engineers is how to determine which IP address is used for the locally-sourced traffic. However, the default scenario can be changed in a couple of different ways and this post is a brief documentation of the available options.&lt;/p&gt;

&lt;h2 id=&#34;the-default-scenario&#34;&gt;The Default Scenario&lt;/h2&gt;

&lt;p&gt;Whenever a local application decides to connect to a remote network endpoint, it creates a network socket, providing a minimal amount of details required to build and send a network packet. Most often, this information includes a destination IP and port number as you can see from the following abbreviated output:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ strace -e trace=network curl http://example.com
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 6
setsockopt(6, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(6, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(6, SOL_TCP, TCP_KEEPIDLE, [60], 4) = 0
setsockopt(6, SOL_TCP, TCP_KEEPINTVL, [60], 4) = 0
connect(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr(&amp;quot;93.184.216.34&amp;quot;)}, 16)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;While this output does not show the DNS resolution part (due to &lt;a href=&#34;https://man7.org/linux/man-pages/man3/getaddrinfo.3.html&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;getaddrinfo()&lt;/code&gt;&lt;/a&gt; not being a syscall), we can see that the only user-specific input information provided by an application (&lt;code&gt;curl&lt;/code&gt;) in the &lt;a href=&#34;https://beej.us/guide/bgnet/html/#connect&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;connect()&lt;/code&gt;&lt;/a&gt; syscall are the remote socket port &lt;code&gt;sin_port&lt;/code&gt; and IP address &lt;code&gt;sin_adddr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What happens next is what we all learned to expect from any operating system, not just Linux:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Destination IP is looked up in the local routing table.&lt;/li&gt;
&lt;li&gt;The resulting route is used to determine the egress interface.&lt;/li&gt;
&lt;li&gt;The IP of that interface is assigned as the source address for the TCP socket.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a sane default that picks an IP address that is most likely to reach the destination, since it&amp;rsquo;s assigned to an egress interface.&lt;/p&gt;

&lt;h2 id=&#34;user-provided-ip&#34;&gt;User-provided IP&lt;/h2&gt;

&lt;p&gt;In some scenarios, when multiple local IPs are reachable outside of the host, users may want to override the default behaviour. A very common use case is to specify an IP address (or interface name) as the traffic source. The following &lt;code&gt;strace&lt;/code&gt; output looks exactly the same as above, with one notable exception:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ strace -e trace=network curl --interface lo http://example.com
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 5
setsockopt(5, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(5, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(5, SOL_TCP, TCP_KEEPIDLE, [60], 4) = 0
setsockopt(5, SOL_TCP, TCP_KEEPINTVL, [60], 4) = 0
setsockopt(5, SOL_SOCKET, SO_BINDTODEVICE, &amp;quot;lo\0&amp;quot;, 3) = 0
connect(5, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr(&amp;quot;93.184.216.34&amp;quot;)}, 16)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;a href=&#34;https://linux.die.net/man/2/setsockopt&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;setsockopt()&lt;/code&gt;&lt;/a&gt; syscall allows clients to bind to a specific interface name using the &lt;code&gt;SO_BINDTODEVICE&lt;/code&gt; option.&lt;/p&gt;

&lt;p&gt;Another alternative would be &lt;a href=&#34;https://beej.us/guide/bgnet/html/#bind&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;bind()&lt;/code&gt;&lt;/a&gt; the client socket to a specific IP address (&lt;code&gt;192.0.2.2&lt;/code&gt; is one of the IPs on &lt;code&gt;lo&lt;/code&gt; interface), which is what &lt;code&gt;curl&lt;/code&gt; does in the following case:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ strace -e trace=network curl --interface 192.0.2.2 http://example.com
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 5
setsockopt(5, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(5, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(5, SOL_TCP, TCP_KEEPIDLE, [60], 4) = 0
setsockopt(5, SOL_TCP, TCP_KEEPINTVL, [60], 4) = 0
setsockopt(5, SOL_SOCKET, SO_BINDTODEVICE, &amp;quot;192.0.2.2\0&amp;quot;, 10) = -1 ENODEV (No such device)
bind(5, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr(&amp;quot;192.0.2.2&amp;quot;)}, 16) = 0
connect(5, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr(&amp;quot;93.184.216.34&amp;quot;)}, 16)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The problem with the above options is that they are application-specific and, thus, require explicit user configuration. While this may work for a small number of applications, in some scenarios it may be easier to have a global setting that would influence this behaviour.&lt;/p&gt;

&lt;h2 id=&#34;netlink-route-source-ip&#34;&gt;Netlink Route Source IP&lt;/h2&gt;

&lt;p&gt;Another available option, that is frequently used on L3 multi-homed network hosts, is the rtnetlink&amp;rsquo;s &lt;code&gt;src&lt;/code&gt; option or &lt;a href=&#34;https://man7.org/linux/man-pages/man7/rtnetlink.7.html&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;RTA_PREFSRC&lt;/code&gt;&lt;/a&gt;. Continuing from the previous example, let&amp;rsquo;s add a static route for the &lt;code&gt;example.com&lt;/code&gt; and specify the &lt;code&gt;src&lt;/code&gt; option with the loopback IP:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ ip route add 93.184.216.34 via 172.20.20.1 src 192.0.2.2
$ ip route get 93.184.216.34
93.184.216.34 via 172.20.20.1 dev eth0 src 192.0.2.2 uid 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we can re-run the original &lt;code&gt;curl&lt;/code&gt; command without specifying the source IP:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ tcpdump -enni eth0 host 93.184.216.34 &amp;amp;
$ strace -e trace=network curl http://example.com
...
connect(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr(&amp;quot;93.184.216.34&amp;quot;)}, 16)
14:19:00.970631 IP 192.0.2.2.33068 &amp;gt; 93.184.216.34.80: Flags [S]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The resulting packet source IP has been changed by the kernel to the IP specified in the &lt;code&gt;ip route add&lt;/code&gt; command above. This option can also be configured by an IP routing daemon, for example, FRR&amp;rsquo;s route-map &lt;a href=&#34;https://docs.frrouting.org/en/stable-9.0/zebra.html#clicmd-set-src-ADDRESS&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;set src&lt;/code&gt;&lt;/a&gt; command or Bird&amp;rsquo;s &lt;a href=&#34;https://bird.network.cz/?get_doc&amp;amp;v=20&amp;amp;f=bird-6.html&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;krt_prefsrc&lt;/code&gt;&lt;/a&gt; configuration option.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Network Automation with CUE - Working with YANG-based APIs</title>
      <link>https://networkop.co.uk/post/2022-12-cue-yang/</link>
      <pubDate>Wed, 07 Dec 2022 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2022-12-cue-yang/</guid>
      <description>

&lt;p&gt;In the &lt;a href=&#34;https://networkop.co.uk/post/2022-11-cue-networking/&#34;&gt;previous post&lt;/a&gt;, I mentioned that CUE can help you work with both &amp;ldquo;industry-standard&amp;rdquo; semi-structured APIs and fully structured APIs where data is modelled using OpenAPI or JSON schema. However, there was an elephant in the room that I conveniently ignored but without which no conversation about network automation would be complete. With this post, I plan to rectify my previous omission and explain how you can use CUE to work with YANG-based APIs. More specifically, I&amp;rsquo;ll focus on OpenConfig and gNMI and show how CUE can be used to write YANG-based configuration data, validate it and send it to a remote device.&lt;/p&gt;

&lt;h2 id=&#34;automating-yang-based-apis-with-cue&#34;&gt;Automating YANG-based APIs with CUE&lt;/h2&gt;

&lt;p&gt;Working with YANG-based APIs is not much different from what I&amp;rsquo;ve described in the two previous blog posts &lt;a href=&#34;https://networkop.co.uk/post/2022-11-cue-ansible/&#34;&gt;[1]&lt;/a&gt; and &lt;a href=&#34;https://networkop.co.uk/post/2022-11-cue-networking/&#34;&gt;[2]&lt;/a&gt;. We&amp;rsquo;re still dealing with structured data that gets assembled based on the rules defined in a set of YANG models and sent over the wire using one of the supported protocols (Netconf, Restconf or gNMI). One of the biggest differences, though, is that data generation gets done in one of the general-purpose programming languages (e.g. Python, Go), since doing it in Ansible is not feasible due to the sheer complexity of YANG schemas. What CUE can bring to the table is the data transformation and generation capabilities often found in general-purpose programming languages while still retaining the simplicity and readability of a DSL.&lt;/p&gt;

&lt;p&gt;If we want to use CUE the main problem that we have to solve is figuring out how to generate the YANG-based CUE definitions. Since YANG is not widely used outside of the physical networking infrastructure space, CUE does not have a native language adaptor for YANG. However, CUE has integrations with a &lt;a href=&#34;https://cuelang.org/docs/integrations/&#34; target=&#34;_blank&#34;&gt;number of&lt;/a&gt; structured data standards which allows us to use one of them as an intermediate step.&lt;/p&gt;

&lt;p&gt;One of the projects that can generate Go language bindings from a set of YANG models is &lt;a href=&#34;https://github.com/openconfig/ygot&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;openconfig/ygot&lt;/code&gt;&lt;/a&gt;. Fortunately, CUE understands Go and can generate its own definitions from Go types using the &lt;code&gt;cue get go [packages]&lt;/code&gt; command. This makes the remainder of the network automation workflow very similar to what I&amp;rsquo;ve described in the &lt;a href=&#34;https://networkop.co.uk/post/2022-11-cue-networking/&#34;&gt;previous post&lt;/a&gt;. We combine CUE definitions with user-provided data, validating its structure and values. Using CUE scripting, we can serialise this data into JSON and orchestrate &lt;a href=&#34;https://gnmic.kmrd.dev/&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;gnmic&lt;/code&gt;&lt;/a&gt; to perform a &lt;a href=&#34;https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#341-the-setrequest-message&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;Set&lt;/code&gt; RPC&lt;/a&gt; with the provided data in the payload.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/cue-yang.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Obviously, if things were that easy, I wouldn&amp;rsquo;t be writing this blog post now. YANG is a complicated language that was designed before our industry converged on a much (relatively) simpler set of schema standards. In the rest of this article, I will document what issues I hit when using the automatically-generated CUE definitions, how I worked around them and what challenges still lie ahead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All code from this blog post can be found in the &lt;a href=&#34;https://github.com/networkop/yang-to-cue&#34; target=&#34;_blank&#34;&gt;yang-to-cue&lt;/a&gt; github repository&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;generating-cue-definitions&#34;&gt;Generating CUE definitions&lt;/h2&gt;

&lt;p&gt;One thing I wanted to get out of the gate is that if you want to use YANG-based APIs, most likely you would need to generate your language bindings or, in my case, CUE definitions automatically. There is absolutely no way you can (or should try to) create them manually. You can look at an &lt;a href=&#34;https://github.com/openconfig/public/blob/master/release/models/interfaces/openconfig-interfaces.yang&#34; target=&#34;_blank&#34;&gt;average YANG model&lt;/a&gt; or a &lt;a href=&#34;https://github.com/PacktPublishing/Network-Automation-with-Go/blob/main/ch08/json-rpc/pkg/srl/srl.go&#34; target=&#34;_blank&#34;&gt;size of the generated library&lt;/a&gt; to understand what level of complexity you are dealing with.&lt;/p&gt;

&lt;p&gt;With that in mind, the only way I could make it work is if I used the &lt;code&gt;cue get go&lt;/code&gt; command, which means the first thing I had to do was generate Go types using the &lt;a href=&#34;https://github.com/openconfig/ygot&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;openconfig/ygot&lt;/code&gt;&lt;/a&gt;. I won&amp;rsquo;t focus on how to do it here, you can see an example in steps 1-3 of the workflow described in the &lt;a href=&#34;https://github.com/networkop/yang-to-cue&#34; target=&#34;_blank&#34;&gt;yang-to-cue&lt;/a&gt; repo or read about it in the &lt;a href=&#34;https://www.packtpub.com/product/network-automation-with-go/9781800560925&#34; target=&#34;_blank&#34;&gt;Go Network Automation book&lt;/a&gt;. Once you have those types defined, you can run the &lt;code&gt;cue get go&lt;/code&gt; command and pull them into your CUE code, for example:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;cue get go yang.to.cue/pkg/...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The above command would generate a &lt;code&gt;[package]_go_gen.cue&lt;/code&gt; file per Go package containing everything that has been recognised and imported. This is where I started seeing issues and below I&amp;rsquo;ll explain what they are and how I fixed them.&lt;/p&gt;

&lt;h3 id=&#34;challenge-1-optional-fields&#34;&gt;Challenge 1 - Optional fields&lt;/h3&gt;

&lt;p&gt;When it comes to field optionality, CUE and YANG have opposite defaults. In YANG each node of a tree is optional by default, while in CUE all fields are mandatory unless they are explicitly marked as optional. When CUE imports definitions from Go types, it looks at each struct field and marks it optional if it is a pointer type. This, however, marks some of the fields as required, which goes against the YANG defaults.&lt;/p&gt;

&lt;p&gt;The simplest solution is to walk through all of the fields defined in all of the structs and make them optional. CUE&amp;rsquo;s Go API includes a convenient helper function that traverses all nodes in a parsed CUE file and allows you to modify their content. Below is a snippet from the &lt;a href=&#34;https://github.com/networkop/yang-to-cue/blob/00f5287a29cf98f1746806e89c5a93b6d2d2d61d/post-import.go&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;post-import.go&lt;/code&gt;&lt;/a&gt; script that does that:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;case *ast.StructLit:
  for _, elt := range x.Elts {
    if field, ok := elt.(*ast.Field); ok {
      name, _, err := ast.LabelName(field.Label)
        if err != nil {
          log.Fatal(err)
        }
        if field.Optional == token.NoPos {
          log.Debugf(&amp;quot;found mandadory field: %s&amp;quot;, name)
          field.Optional = token.Blank.Pos()
        }
      }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This was the simplest way to work around the problem. The downside is that we lose the ability to check if any field was marked mandatory by a YANG model. Fortunately, for this we first need to wait for &lt;code&gt;ygot&lt;/code&gt; to implement &lt;a href=&#34;https://github.com/openconfig/ygot/issues/514&#34; target=&#34;_blank&#34;&gt;this functionality&lt;/a&gt;, by which time CUE&amp;rsquo;s &lt;a href=&#34;https://github.com/cue-lang/proposal/blob/main/designs/1951-required-fields-v2.md&#34; target=&#34;_blank&#34;&gt;mandatory field proposal&lt;/a&gt; may get implemented as well, making the future solution a bit easier.&lt;/p&gt;

&lt;h3 id=&#34;challenge-2-enums&#34;&gt;Challenge 2 - ENUMs&lt;/h3&gt;

&lt;p&gt;The second problem is caused by the way the &lt;a href=&#34;https://github.com/openconfig/ygot&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;openconfig/ygot&lt;/code&gt;&lt;/a&gt; deals with YANG enum types. Most enum types I&amp;rsquo;ve seen are aliases to &lt;code&gt;int64&lt;/code&gt; and each enum value is a constant (of enum type) that stores that &lt;a href=&#34;https://www.rfc-editor.org/rfc/rfc7950#section-9.6.4.2&#34; target=&#34;_blank&#34;&gt;enum&amp;rsquo;s value&lt;/a&gt;. When emitting the JSON value, &lt;code&gt;ygot&lt;/code&gt; uses the constant to perform a lookup in the &lt;code&gt;ΛEnum&lt;/code&gt; dictionary storing the actual enum name. The following excerpt from &lt;a href=&#34;https://github.com/networkop/yang-to-cue/blob/00f5287a29cf98f1746806e89c5a93b6d2d2d61d/pkg/yang.go&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;yang-to-go/pkg/yang.go&lt;/code&gt;&lt;/a&gt; file should make it a bit clearer:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;type E_AristaIntfAugments_AristaAddrType int64

const (
  AristaIntfAugments_AristaAddrType_UNSET E_AristaIntfAugments_AristaAddrType = 0
  ...
  AristaIntfAugments_AristaAddrType_IPV6 E_AristaIntfAugments_AristaAddrType = 3
)
var ΛEnum = map[string]map[int64]ygot.EnumDefinition{
  &amp;quot;E_AristaIntfAugments_AristaAddrType&amp;quot;: {
    1: {Name: &amp;quot;PRIMARY&amp;quot;},
    2: {Name: &amp;quot;SECONDARY&amp;quot;},
    3: {Name: &amp;quot;IPV6&amp;quot;},
  },
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By default, CUE would ingest all enum types and store them as integers and wouldn&amp;rsquo;t know anything about the above map or its string values. So what I had to do was parse the auto-generated CUE file and patch the enum definitions by replacing integers (enum&amp;rsquo;s value) with strings (enum&amp;rsquo;s name) from the &lt;code&gt;ΛEnum&lt;/code&gt; map. All this is done inside the same &lt;a href=&#34;https://github.com/networkop/yang-to-cue/blob/master/post-import.go#L208-L264&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;post-import.go&lt;/code&gt;&lt;/a&gt; script and the resulting CUE code looks something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-javascript&#34;&gt;#enumE_AristaIntfAugments_AristaAddrType:
  #AristaIntfAugments_AristaAddrType_UNSET |
  #AristaIntfAugments_AristaAddrType_PRIMARY |
  #AristaIntfAugments_AristaAddrType_SECONDARY |
  #AristaIntfAugments_AristaAddrType_IPV6

#E_AristaIntfAugments_AristaAddrType: string

#AristaIntfAugments_AristaAddrType_UNSET: 
    { #E_AristaIntfAugments_AristaAddrType &amp;amp; &amp;quot;UNSET&amp;quot; }
#AristaIntfAugments_AristaAddrType_PRIMARY: 
    { #E_AristaIntfAugments_AristaAddrType &amp;amp; &amp;quot;PRIMARY&amp;quot; }
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This definition would allow you to write values using concrete value strings, e.g. &lt;code&gt;&amp;quot;addr-type&amp;quot;: &amp;quot;PRIMARY&amp;quot;&lt;/code&gt; or simply refer to one of the globally defined constants, as in the following example from the &lt;a href=&#34;https://github.com/networkop/yang-to-cue/blob/master/values.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;yang-to-cue/values.cue&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-javascript&#34;&gt;config: {
  &amp;quot;addr-type&amp;quot;: oc.#AristaIntfAugments_AristaAddrType_PRIMARY
  &amp;quot;prefix-length&amp;quot;: 24
  ip: &amp;quot;192.0.2.1&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;challenge-3-yang-lists&#34;&gt;Challenge 3 - YANG lists&lt;/h3&gt;

&lt;p&gt;This ended up being the biggest challenge I had to solve. For all intents and purposes, a YANG list is a map (or a dictionary) with values identified by unique keys. So &lt;a href=&#34;https://github.com/openconfig/ygot&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;openconfig/ygot&lt;/code&gt;&lt;/a&gt; naturally stores YANG lists as Go maps. This makes it easier to ensure uniqueness and catch any duplicates. However, on the wire, a YANG list is represented as a list of objects (&lt;code&gt;[...{}]&lt;/code&gt;), so when it&amp;rsquo;s time to emit a payload, &lt;code&gt;ygot&lt;/code&gt; &lt;a href=&#34;https://github.com/openconfig/ygot/blob/master/ygot/render.go#L1281&#34; target=&#34;_blank&#34;&gt;translates&lt;/a&gt; maps to lists, producing a valid RFC7951 JSON.&lt;/p&gt;

&lt;p&gt;This last bit is unique to &lt;code&gt;ygot&lt;/code&gt;&amp;rsquo;s serialization logic and by default remains unknown to CUE. So I&amp;rsquo;ve taken the most straightforward approach and converted all maps to lists before running the &lt;code&gt;cue get go&lt;/code&gt; command. This is described in the readme of the &lt;a href=&#34;https://github.com/networkop/yang-to-cue&#34; target=&#34;_blank&#34;&gt;yang-to-cue&lt;/a&gt; repository and can be accomplished with a little bit of &lt;code&gt;sed&lt;/code&gt; magic:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sed -i -E &#39;s/map\[.*\]\*(\S+)/\[\]\*\1/&#39; pkg/yang.go
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;While this solves the problem of helping CUE generate a valid RFC7951 JSON, this does not guarantee YANG list entry uniqueness, leaving room for error. Fortunately, it&amp;rsquo;s possible to use CUE itself to introduce additional constraints and ensure all entries in a list are unique.&lt;/p&gt;

&lt;p&gt;In the following example, I&amp;rsquo;m using a hidden field &lt;code&gt;_check&lt;/code&gt; to store a set of YANG keys and compare its length to the length of the corresponding YANG list. As long as the list and a set of its keys have the same size, the validation passes and a payload is emitted by CUE.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-javascript&#34;&gt;#OpenconfigInterfaces_Interfaces: {
  interface: [...null | #OpenconfigInterfaces_Interfaces_Interface]
  _check: {
    for intf in interface {
      let key = intf.name
      &amp;quot;\(key)&amp;quot;: true
    }
  }
  if len(_check) != len(interface) {_|_}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The above code snippet is automatically injected into every YANG list definition in CUE when the &lt;a href=&#34;https://github.com/networkop/yang-to-cue/blob/00f5287a29cf98f1746806e89c5a93b6d2d2d61d/post-import.go&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;post-import.go&lt;/code&gt;&lt;/a&gt; is run with the default &lt;code&gt;-yanglist=true&lt;/code&gt; argument. The actual &lt;a href=&#34;https://github.com/networkop/yang-to-cue/blob/master/post-import.go#L189-L200&#34; target=&#34;_blank&#34;&gt;injected code&lt;/a&gt; is slightly more complicated to account for the presence of composite keys (keys with more than one value) and includes a check that &lt;code&gt;entry.key&lt;/code&gt; is always the same as &lt;code&gt;entry.config.key&lt;/code&gt; as &lt;a href=&#34;https://www.openconfig.net/docs/guides/style_guide/#list&#34; target=&#34;_blank&#34;&gt;required&lt;/a&gt; by the Openconfig styling guide.&lt;/p&gt;

&lt;h2 id=&#34;outro&#34;&gt;Outro&lt;/h2&gt;

&lt;p&gt;So where does all of the above leave us in relation to CUE and YANG? So far I was able to generate some pretty sizeable instances of YANG using  CUE and apply validation rules imported from &lt;code&gt;ygot&lt;/code&gt; packages. This makes me pretty comfortable I&amp;rsquo;ve reached the 80% feature coverage target I was aiming for a &lt;a href=&#34;https://twitter.com/networkop1/status/1550145828236443648&#34; target=&#34;_blank&#34;&gt;few months ago&lt;/a&gt;. Here&amp;rsquo;s an example from the &lt;a href=&#34;https://github.com/networkop/yang-to-cue&#34; target=&#34;_blank&#34;&gt;yang-to-cue&lt;/a&gt; repo that you can successfully apply to any reachable Arista EOS device using the &lt;code&gt;cue apply&lt;/code&gt; command.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-javascript&#34;&gt;package main

import oc &amp;quot;yang.to.cue/pkg:yang&amp;quot;

config: oc.#Device &amp;amp; {
  interfaces: interface: [{
    config: {
      description: &amp;quot;loopback interface&amp;quot;
      mtu:         1500
      name:        &amp;quot;Loopback0&amp;quot;
    }
    name: &amp;quot;Loopback0&amp;quot;
    subinterfaces: {
      subinterface: [{
        config: {
          description: &amp;quot;default subinterface&amp;quot;
          index:       0
        }
        index: 0
        ipv4: {
          addresses: {
            address: [{
              ip: &amp;quot;192.0.2.1&amp;quot;
              config: {
                &amp;quot;addr-type&amp;quot;:     oc.#AristaIntfAugments_AristaAddrType_PRIMARY
                &amp;quot;prefix-length&amp;quot;: 24
                ip:              &amp;quot;192.0.2.1&amp;quot;
              }
            }]
          }
        }
      }]
    }
  }]
  &amp;quot;network-instances&amp;quot;: &amp;quot;network-instance&amp;quot;: [{
    config: name: &amp;quot;default&amp;quot;
    name: &amp;quot;default&amp;quot;
    protocols: protocol: [{
      bgp: {
        global: config: as: 65000
        neighbors: neighbor: [{
          &amp;quot;afi-safis&amp;quot;: &amp;quot;afi-safi&amp;quot;: [{
            &amp;quot;afi-safi-name&amp;quot;: oc.#OpenconfigBgpTypes_AFI_SAFI_TYPE_IPV4_UNICAST
            config: &amp;quot;afi-safi-name&amp;quot;: &amp;quot;IPV4_UNICAST&amp;quot;
          }]
          config: {
            &amp;quot;neighbor-address&amp;quot;: &amp;quot;169.254.0.1&amp;quot;
            &amp;quot;peer-as&amp;quot;:          65001
          }
          &amp;quot;neighbor-address&amp;quot;: &amp;quot;169.254.0.1&amp;quot;
        }]
      }
      config: {
        identifier: &amp;quot;BGP&amp;quot;
        name:       &amp;quot;BGP&amp;quot;
      }
      identifier: &amp;quot;BGP&amp;quot;
      name:       &amp;quot;BGP&amp;quot;
    }]
  }]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can use the approach described in this blog post to write and validate YANG-compliant data entirely in CUE and, once CUE gets its own &lt;a href=&#34;https://github.com/cue-lang/cue/issues/142&#34; target=&#34;_blank&#34;&gt;language server&lt;/a&gt;, writing this data would become even easier with IDE hints, autocompletion and error highlighting. Combine this with data generation and scripting capabilities described in the &lt;a href=&#34;https://networkop.co.uk/post/2022-11-cue-networking/&#34;&gt;previous post&lt;/a&gt; and this gives you a versatile and robust toolset to work with YANG-based APIs, something that has been missing for a very long time.&lt;/p&gt;

&lt;p&gt;There are still a few areas for improvement where CUE does not yet do as good a job as it could. One of them is error reporting in the YANG list validation logic. There&amp;rsquo;s no way to emit a custom error message, however, this may change once &lt;a href=&#34;https://github.com/cue-lang/cue/issues/943&#34; target=&#34;_blank&#34;&gt;this proposal&lt;/a&gt; gets implemented. Another area for improvement could be extracting more metadata from Go types, but this seems to be unique to YANG/ygot so unlikely to get implemented in CUE natively. That being said, I hope that the approach that I&amp;rsquo;ve shown here &amp;ndash; importing Go types using CUE and changing them later with a Go script &amp;ndash; would work for most of the potential future improvements.&lt;/p&gt;

&lt;p&gt;Since CUE is a pre-1.0 language, I would expect a few more things to change in the coming months. I doubt these changes would have any major negative impact on &lt;a href=&#34;http://localhost:1313/tags/cue/&#34; target=&#34;_blank&#34;&gt;what I&amp;rsquo;ve written about CUE&lt;/a&gt; so far. If anything, they would improve the language, like the &lt;a href=&#34;https://github.com/cue-lang/cue/issues/165&#34; target=&#34;_blank&#34;&gt;query proposal&lt;/a&gt; that would simplify CUE&amp;rsquo;s data generation capabilities or the &lt;a href=&#34;https://github.com/cue-lang/cue/issues/2007&#34; target=&#34;_blank&#34;&gt;function signatures proposal&lt;/a&gt; to allow external, user-provided code to be injected into the CUE evaluation process. So in my view now is the right time to start exploring CUE and injecting it into various parts of your network automation workflow. As you dig more into the details of the language, you&amp;rsquo;ll discover more interesting patterns and applications and, hopefully, CUE (Configure, Unify, Execute) becomes that common language for configuration and data, unifying different parts of IT infrastructure.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Network Automation with CUE - Advanced workflows</title>
      <link>https://networkop.co.uk/post/2022-11-cue-networking/</link>
      <pubDate>Tue, 22 Nov 2022 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2022-11-cue-networking/</guid>
      <description>

&lt;p&gt;What I&amp;rsquo;ve covered in the &lt;a href=&#34;https://networkop.co.uk/post/2022-11-cue-ansible/&#34;&gt;previous blog post&lt;/a&gt; about CUE and Ansible were isolated use cases, disconnected islands in the sea of network automation. The idea behind that was to simplify the introduction of CUE into existing network automation workflows. However, this does not mean CUE is limited to those use cases and, in fact, CUE is most powerful when it&amp;rsquo;s used end-to-end &amp;mdash; both to generate device configurations and to orchestrate interactions with external systems. In this post, I&amp;rsquo;m going to demonstrate how to use CUE for advanced network automation workflows involving fetching information from an external device inventory management system, using it to build complex hierarchical configuration values and, finally, generating and pushing intended configurations to remote network devices.&lt;/p&gt;

&lt;h2 id=&#34;cue-vs-cue-scripting&#34;&gt;CUE vs CUE scripting&lt;/h2&gt;

&lt;p&gt;CUE was designed to be a simple, scalable and robust configuration language. This is why it includes type checking, schema and constraints validation as first-class constructs. There are some &lt;a href=&#34;https://cuelang.org/docs/usecases/configuration/&#34; target=&#34;_blank&#34;&gt;design decisions&lt;/a&gt;, like the lack of inheritance or value overrides, that may take new users by surprise, however over time it becomes clear that they make the language simpler and more readable. One of the most interesting features of CUE, though, is that all code is hermetic. What that means is all configuration values must come from local CUE files and cannot be dynamically fetched or injected into the evaluation process, so that no matter how many times or in which environment you run your CUE code, it always produces the same result.&lt;/p&gt;

&lt;p&gt;However, as we all know, in real life configuration values may come from many different places. In the network automation context, we often use IP address and infrastructure management systems (IPAM/DCIM) to store device-specific data, often referring to these systems as a &amp;ldquo;source of truth&amp;rdquo;. I won&amp;rsquo;t focus on the fact that most often these systems are managed imperatively (point and click), making them a very poor choice for this task (how do you roll back?), but their dominance and popularity in our industry are undeniable. So how can we make CUE work in such environments?&lt;/p&gt;

&lt;p&gt;CUE has an optional scripting layer, that is complementary to the core functionality of a configuration language. The CUE scripting (or &lt;a href=&#34;(https://cuelang.org/docs/usecases/configuration/#tooling)&#34; target=&#34;_blank&#34;&gt;tooling&lt;/a&gt;) layer works by evaluating files (identified by the &lt;code&gt;_tool.cue&lt;/code&gt; suffix) that contain a set of tasks and executing them concurrently. These files are still written in CUE and can access the values defined in the rest of the CUE module, however, CUE tasks &lt;em&gt;are&lt;/em&gt; allowed to make local and remote I/O calls and can be strung together to form some pretty complex workflows. As you may have guessed, this is what allows us to interact with external databases and remote network devices.&lt;/p&gt;

&lt;h2 id=&#34;advanced-network-automation-workflow&#34;&gt;Advanced Network Automation Workflow&lt;/h2&gt;

&lt;p&gt;Let&amp;rsquo;s revisit the advanced network automation workflow, that was described in the &lt;a href=&#34;https://networkop.co.uk/post/2022-10-cue-intro/&#34;&gt;CUE introduction post&lt;/a&gt;. What makes it different from the intermediate workflow is that  host variables are sourced from multiple different places. In most common workflows, these places can be described as:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Local static variables, defined in host and group variables.&lt;/li&gt;
&lt;li&gt;Variables injected by the environment, which often include sensitive information like secrets and passwords.&lt;/li&gt;
&lt;li&gt;Externally-sourced data, fetched and evaluated during runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once this data is collected and evaluated, the remainder of the process looks very similar to what I&amp;rsquo;ve described in the &lt;a href=&#34;https://networkop.co.uk/post/2022-11-cue-ansible/&#34;&gt;previous blog post&lt;/a&gt;, i.e. this data is modified and expanded to generate a complete per-device set of variables which are then used to produce the final device configuration. The top part of the following diagrams is a visual representation of this workflow.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/cue-advanced.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;The bottom part shows how the same data sources are consumed in the equivalent CUE workflow. External data from IPAM/DCIM systems is ingested using the CUE scripting layer and saved next to the rest of the CUE values. CUE runtime now takes the latest snapshot of external data, combines it with other local CUE values and generates a set of per-device configurations. At this point, we can either apply them as-is or combine them with Jinja templates to generate a semi-structured text before sending it to the remote device.&lt;/p&gt;

&lt;p&gt;In the rest of this blog post, I will cover some of the highlights of the above CUE workflow, while configuring an unnumbered BGP session between Arista cEOS and NVIDIA Cumulus Linux connected back-to-back. The goal is to show an example of how the data flows from its source all the way to its ultimate destination and how CUE can be used at every step of the way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All code from this blog post can be found in the &lt;a href=&#34;https://github.com/networkop/cue-networking-II&#34; target=&#34;_blank&#34;&gt;cue-networking-II&lt;/a&gt; github repository&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;pulling-configuration-data-from-external-systems&#34;&gt;Pulling Configuration Data from External Systems&lt;/h2&gt;

&lt;p&gt;For an external IPAM/DCIM system I&amp;rsquo;ll be using the public demo instance of &lt;a href=&#34;https://github.com/nautobot/nautobot&#34; target=&#34;_blank&#34;&gt;Nautobot&lt;/a&gt; located at &lt;a href=&#34;https://demo.nautobot.com/&#34; target=&#34;_blank&#34;&gt;demo.nautbot.com&lt;/a&gt;. Since this is a demo instance, it gets rebuilt periodically, so I need to pre-populate it with the required device data. This is done based on the static &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/inventory/inventory.cue&#34; target=&#34;_blank&#34;&gt;inventory file&lt;/a&gt; and automated with the &lt;code&gt;cue apply ./...&lt;/code&gt; command. The action of populating IPAM/DCIM systems with data is normally a day 0 exercise and is rarely included in day 1+ network automation workflows, so I won&amp;rsquo;t focus on it here. However, if you&amp;rsquo;re interested in an advanced REST API workflow orchestrated by CUE, you can check out the &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/seed_tool.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;seed_tool.cue&lt;/code&gt;&lt;/a&gt; file for more details.&lt;/p&gt;

&lt;p&gt;Once we have the right data in Nautobot, we can fetch it by orchestrating a number of REST API calls with CUE. However, since Nautobot supports GraphQL, I&amp;rsquo;ll cheat a little bit and get all the data in a single RPC. The &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/query.gql&#34; target=&#34;_blank&#34;&gt;query itself&lt;/a&gt; is less important, as it&amp;rsquo;s unique to my specific requirements, so I&amp;rsquo;ll focus only on the CUE code. In the &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/fetch_tool.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;fetch_tool.cue&lt;/code&gt;&lt;/a&gt; file I define a sequence of tasks that will get executed concurrently for all devices from the &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/inventory/inventory.cue#L14&#34; target=&#34;_blank&#34;&gt;inventory&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Query the GraphQL API endpoint of Nautobot and unmarshal the response into a CUE struct.&lt;/li&gt;
&lt;li&gt;Import the received data as CUE and save it in a device-specific directory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of the above can be done with a single &lt;code&gt;cue fetch ./...&lt;/code&gt; command and the following snippet shows how the first task is written in CUE:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;import (
	&amp;quot;text/template&amp;quot;
	&amp;quot;tool/http&amp;quot;
	&amp;quot;encoding/json&amp;quot;
)

command: fetch: {
 for _, dev in inventory.#devices {
  (dev.name): {
   gqlRequest: http.Post &amp;amp; {
    url:     inventory.ipam.url + &amp;quot;/graphql/&amp;quot;
    request: inventory.ipam.headers &amp;amp; {
     body: json.Marshal({
      query: template.Execute(gqlQuery.contents, {name: dev.name})
     })
    }
   }

   response: json.Unmarshal(gqlRequest.response.body)

   // save data in a file (omitted for brevity)
  }
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The above code snippet demonstrates how to make a single HTTP API call and parse the received payload using &lt;code&gt;tool/http&lt;/code&gt; and &lt;code&gt;encoding/json&lt;/code&gt; packages from the CUE&amp;rsquo;s &lt;a href=&#34;https://pkg.go.dev/cuelang.org/go@v0.4.3/pkg&#34; target=&#34;_blank&#34;&gt;standard library&lt;/a&gt;. The CUE scripting layer is smart enough to understand dependencies between tasks, e.g. in this case &lt;code&gt;json.Unmarshal&lt;/code&gt; will only be called once the &lt;code&gt;gqlRequest&lt;/code&gt; has returned a response, while still trying to run tasks concurrently (all GraphQL calls will be made at roughly the same time). This makes it highly efficient at almost no cost to the end user.&lt;/p&gt;

&lt;h2 id=&#34;data-transformation&#34;&gt;Data Transformation&lt;/h2&gt;

&lt;p&gt;At this point, it would make sense to talk a little about how CUE evaluates files from a hierarchical directory structure. In Ansible, it&amp;rsquo;s common to use &amp;ldquo;group&amp;rdquo; variables to manage settings common amongst multiple hosts. In CUE, you can use subdirectories to group related hosts and manage their common configuration values. Although my two-node test topology is not the best example for this, I still tried to group data based on the &lt;code&gt;device role&lt;/code&gt; value extracted from Nautobot. This is what the &lt;code&gt;./config&lt;/code&gt; directory structure looks like. As you can see, host-specific CUE files are sitting in leaf/edge directories, while common data values and operations are defined in their parent directories:&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/cue-dirs.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Whenever a CUE script needs to evaluate data from one of these subdirectories (for example &lt;code&gt;./...&lt;/code&gt; tells CUE to evaluate all files recursively starting from the current directory), the values in the leaf subdirectories get merged with everything from their parents. So, for example, the &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/config/lleaf/lon-sw-01/lon-sw-01.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;lon-sw-01.cue&lt;/code&gt;&lt;/a&gt; values will get merged with &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/config/lleaf/groupvars.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;./lleaf/groupvars.cue&lt;/code&gt;&lt;/a&gt; but not with &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/config/sspine/groupvars.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;sspine/groupvars.cue&lt;/code&gt;&lt;/a&gt;, which will get merged with &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/config/sspine/lon-sw-02/lon-sw-02.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;lon-sw-02.cue&lt;/code&gt;&lt;/a&gt;. This is just an example of how to optimise configuration values to remove boilerplate, you can check out my earlier &lt;a href=&#34;https://github.com/networkop/cue-networking&#34; target=&#34;_blank&#34;&gt;cue-networking&lt;/a&gt; repository for a more complete real-world example.&lt;/p&gt;

&lt;p&gt;So now in the leaf CUE files we&amp;rsquo;ve got the data that was retrieved from Nautobot, saved in a &lt;code&gt;hostvars: [device name]: {}&lt;/code&gt; struct. That means in the topmost &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/config/hostvars.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;hostvars.cue&lt;/code&gt;&lt;/a&gt; file I&amp;rsquo;ve got access to all of that data and can start adding a schema and even do some initial value computations. You can view the resulting host variables with the &lt;code&gt;cue try ./...&lt;/code&gt; command.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;$ cue try ./...
-== hostvars[lon-sw-02] ==-
name: lon-sw-02
device_role:
  name: sspine
&amp;gt; snip &amp;lt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The majority of the work is done in the &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/config/transform.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;transform.cue&lt;/code&gt;&lt;/a&gt; file, where &lt;code&gt;hostvars&lt;/code&gt; get transformed into a complete structured device configuration. As I&amp;rsquo;ve already covered data transformation in the &lt;a href=&#34;https://networkop.co.uk/post/2022-11-cue-ansible/&#34;&gt;previous blog post&lt;/a&gt;, I won&amp;rsquo;t focus too much on it here, and invite you to walk through &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/config/transform.cue&#34; target=&#34;_blank&#34;&gt;the code&lt;/a&gt; on your own. However, before moving on, I want to discuss the use of schemas in the data transformation logic, e.g. &lt;code&gt;nvidia.#set&lt;/code&gt; in the below code snippet from the &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/config/transform.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;transform.cue&lt;/code&gt;&lt;/a&gt; file:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;nvidiaX: {
  _input: {}
  nvidia.#set &amp;amp; {
    interface: {
      for _, intf in _input.interfaces {
        if strings.HasPrefix(intf.name, &amp;quot;loopback&amp;quot;) {
          lo: {
            ip: address: (intf.ip_addresses[0].address): {}
            type: &amp;quot;loopback&amp;quot;
// omitted for brevity
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Although schemas are optional, they can give you additional assurance that what you&amp;rsquo;re doing is right and catch errors before you try to use the generated data. Moreover, once CUE gets its own &lt;a href=&#34;https://github.com/cue-lang/cue/issues/142&#34; target=&#34;_blank&#34;&gt;language server&lt;/a&gt;, writing the code would become a lot easier with IDE&amp;rsquo;s help. Similar to Go, you would get features like struct templates, autocompletion and error highlighting.&lt;/p&gt;

&lt;p&gt;The biggest problem with using a schema is generating it in the first place. I&amp;rsquo;ve briefly touched upon this subject in the &lt;a href=&#34;http://localhost:1313/post/2022-11-cue-ansible/#input-data-validation&#34; target=&#34;_blank&#34;&gt;previous blog post&lt;/a&gt; but want to expand a bit on it here. Doesn&amp;rsquo;t matter if you work with a &lt;a href=&#34;https://docs.nvidia.com/networking-ethernet-software/cumulus-linux-44/api/index.html&#34; target=&#34;_blank&#34;&gt;model-compliant API&lt;/a&gt; (OpenAPI or YANG) or with &lt;a href=&#34;(https://github.com/aristanetworks/ansible-avd/tree/devel/ansible_collections/arista/avd/roles/eos_cli_config_gen/templates/eos)&#34; target=&#34;_blank&#34;&gt;templates&lt;/a&gt; that generate a semi-structured set of CLI commands, you can always describe their input with a data model. CUE understands a few common schema languages and can import and generate its own definitions from them. So now all that we need to do is generate that data model somehow.&lt;/p&gt;

&lt;p&gt;In some cases, you may be in luck if your vendor already publishes these models, however, this time I&amp;rsquo;ll focus on how to generate them manually. The detailed step-by-step process is &lt;a href=&#34;https://github.com/networkop/cue-networking-II#creating-cue-schemas&#34; target=&#34;_blank&#34;&gt;documented&lt;/a&gt; in the GitHub repository, but here I want to summarise some of the key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your device manages its configuration as structured data (the case of NVIDIA Cumulus Linux), you can generate a JSON schema from an existing configuration instance. For example, I&amp;rsquo;ve worked out the exact set of values I need to configure first, saved it in a YAML file and ran it through YAML to JSON schema &lt;a href=&#34;https://jsonformatter.org/yaml-to-jsonschema&#34; target=&#34;_blank&#34;&gt;converter&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If you have to use text templates to produce the device config (the case of Arisa EOS), you can infer a JSON schema from a Jinja template (see &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/main/schemas/jinja-to-json-schema.py&#34; target=&#34;_blank&#34;&gt;this script&lt;/a&gt; for an example).&lt;/li&gt;
&lt;li&gt;CUE can correctly recognise the JSON schema format and import it as native definitions using the &lt;code&gt;cue import&lt;/code&gt; command.&lt;/li&gt;
&lt;li&gt;Following the initial (double) conversion, some of the type information may get lost or distorted, so most likely you would need to massage the automatically generated CUE schema before you can use it. This, however, only needs to be done once, since you can discard the intermediate schema files and carry on working exclusively with CUE definitions from now on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can view the generated structured device configurations, produced by the data transformation logic, by running the &lt;code&gt;cue show ./...&lt;/code&gt; command.&lt;/p&gt;

&lt;h2 id=&#34;configuration-push&#34;&gt;Configuration Push&lt;/h2&gt;

&lt;p&gt;This is the final stage of the CUE workflow where, once again, I use CUE scripting to interact with Arista&amp;rsquo;s JSON RPC and NVIDIA&amp;rsquo;s REST APIs. All that is done as a part of a user-defined &lt;code&gt;cue push ./...&lt;/code&gt; command that executes multiple vendor-dependent workflows in per-device coroutines. You can find the complete implementation in the &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/main_tool.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;main_tool.cue&lt;/code&gt;&lt;/a&gt; file, and here I&amp;rsquo;d like to zoom in on a few interesting concepts.&lt;/p&gt;

&lt;p&gt;The first one is authentication and secret management. As I&amp;rsquo;ve mentioned before, one of the common ways of injecting secrets is via environment variables, e.g. if you running a workflow inside a CI/CD system. While CUE cannot inject them natively, you can achieve the same result using the &lt;code&gt;@tag&lt;/code&gt; keyword. A common pattern is to define default values that can be overridden with a user-provided command line tag, like in the following snippet from the &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/inventory/inventory.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;inventory.cue&lt;/code&gt;&lt;/a&gt; file:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;auth: {
  nvidia: {
    user:     *&amp;quot;cumulus&amp;quot; | string @tag(nvidia_user)
    password: *&amp;quot;cumulus&amp;quot; | string @tag(nvidia_pwd)
  }
  arista: {
    user:     *&amp;quot;admin&amp;quot; | string @tag(arista_user)
    password: *&amp;quot;admin&amp;quot; | string @tag(arista_pwd)
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When calling any CUE script, you can now pass an additional &lt;code&gt;-t tag_name=tag_value&lt;/code&gt; flag that will get injected into your code. For example, this is how I would change the default password for Arista:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;export ARISTA_PWD=foo
cue push -t arista_pwd=$ARISTA_PWD ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Another interesting concept is the &lt;a href=&#34;https://cuetorials.com/patterns/functions/&#34; target=&#34;_blank&#34;&gt;function pattern&lt;/a&gt;. It&amp;rsquo;s an ability to abstract a reusable piece of CUE code in a dedicated struct that can be evaluated when needed by any number of callers. I&amp;rsquo;ve used this pattern multiple times in most of the &lt;code&gt;_tool.cue&lt;/code&gt; files, but below I&amp;rsquo;ll cover its simplest form.&lt;/p&gt;

&lt;p&gt;Before we can send the generated configuration to Arista eAPI endpoint, we need to wrap it with a few special keywords &amp;ndash; &lt;code&gt;enable&lt;/code&gt;, &lt;code&gt;configure&lt;/code&gt; and &lt;code&gt;write&lt;/code&gt;. This is done in a special struct called &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/main_tool.cue#L60&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;eapi_wrapper&lt;/code&gt;&lt;/a&gt;. This struct defines an abstract schema for its input (a list of strings) but performs some concrete actions on it (wraps it in special keywords). In order to &amp;ldquo;call&amp;rdquo; this &amp;ldquo;function&amp;rdquo; we unify it with a struct that we know will define these inputs as concrete values. CUE runtime will delay the evaluation of this function struct until all of its inputs are known. In the following example, once CUE generates a list of CLI commands in the &lt;code&gt;split_commands&lt;/code&gt; list, it will evaluate the &amp;ldquo;function call&amp;rdquo; expression and the result will become available to subsequent tasks in &lt;code&gt;wrapped_commands.output&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;eapi_wrapper: {
  input: [...string]
  output: [&amp;quot;enable&amp;quot;, &amp;quot;configure&amp;quot;] + input + [&amp;quot;write&amp;quot;]
}

command: push: {
  for _, dev in inventory.#devices {
    (dev.name): {
      // ...
      wrapped_commands: eapi_wrapper &amp;amp; {input: split_commands}
      // ...
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The last concept I wanted to cover is the sequencing of tasks in CUE scripts. As I&amp;rsquo;ve mentioned before, CUE runtime is able to infer the implicit dependencies between tasks and evaluate them in the right order. This happens when an input of one task consumes an output from another task. This way you can just focus on writing code, while CUE will do its best to parallelise as many tasks as it can.&lt;/p&gt;

&lt;p&gt;However, some tasks don&amp;rsquo;t have implicit dependencies but still need to be run in sequence. A good example of this is the interaction with NVIDIA&amp;rsquo;s NVUE API. The procedure to apply the generated configuration consists of 3 stages &amp;ndash; (1) creating a new configuration revision, (2) patching this revision with the generated data and (3) applying it. While 1-2 and 1-3 have implicit dependencies (revision ID generated in 1), stages 2 and 3 don&amp;rsquo;t, but 3 must always happen after 2. The way we can make it happen is by adding &lt;code&gt;$after&lt;/code&gt; to the third task, referencing the name of the second. This little trick allows CUE to build the right graph of dependencies and apply the revision only after it has been patched.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;createRevision: http.Post &amp;amp; {
  url: &amp;quot;https://\(dev.name):8765/nvue_v1/revision&amp;quot;
  // ...
}

patchRevision: http.Do &amp;amp; {
  method: &amp;quot;PATCH&amp;quot;
  url:    &amp;quot;https://\(dev.name):8765/nvue_v1/?rev=\(escapedID)&amp;quot;
  // ...
}

applyRevision: http.Do &amp;amp; {
  $after: patchRevision
  method: &amp;quot;PATCH&amp;quot;
  url:    &amp;quot;https://\(dev.name):8765/nvue_v1/revision/\(escapedID)&amp;quot;
  // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can see the complete example of the last two concepts in the &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/main_tool.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;main_tool.cue&lt;/code&gt;&lt;/a&gt; file and a few more advanced workflows in &lt;a href=&#34;https://github.com/networkop/cue-networking-II/blob/64064138005dc55b9fb7a0e5c3b3f9a55eecfdd0/seed_tool.cue&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;seed_tool.cue&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&#34;outro&#34;&gt;Outro&lt;/h2&gt;

&lt;p&gt;You can test the complete CUE workflow in a virtual environment with the help of &lt;a href=&#34;https://containerlab.dev/quickstart/&#34; target=&#34;_blank&#34;&gt;containerlab&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the lab with &lt;code&gt;cue lab-up ./...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Pre-seed the demo Nautobot instance with &lt;code&gt;cue apply ./...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Import the data from Nautobot with &lt;code&gt;cue fetch ./...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Push the generated device configs with &lt;code&gt;cue push ./...&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can verify that everything works as intended by pinging the peer device&amp;rsquo;s loopback, e.g. &lt;code&gt;docker exec lon-sw-01 ping 198.51.100.2&lt;/code&gt;. More importantly, at this stage, we have managed to replace all functions of Ansible, while having improved the data integrity, added flexibility and made our network automation workflow more robust.&lt;/p&gt;

&lt;p&gt;Another interesting bonus of using CUE, when compared to Ansible, is the reduced resource utilisation. Due to a completely different architecture, CUE consumes a lot fewer resources and works much faster than Ansible, while doing essentially the same work. I&amp;rsquo;ve done some measurements of how CUE compares to Ansible when doing remote machine execution (running commands via SSH) and making remote API calls and in both cases CUE outperforms Ansible across major dimensions. In the most extreme case (CUE API vs Ansible API), CUE is more than 3 times faster and consumes less than 8% of the memory required by Ansible. You can find this and other results in the &lt;a href=&#34;https://github.com/networkop/cue-ansible&#34; target=&#34;_blank&#34;&gt;cue-ansible&lt;/a&gt; repository.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;I think at this point I&amp;rsquo;ve covered all that I wanted about CUE and how it can be used for common network automation workflows. My hope is that people can see that there is a better alternative to what we use today and keep an open mind when making their next decision.&lt;/p&gt;

&lt;p&gt;If you feel like this is something unfamiliar and strange, remember that Ansible and Python all used to feel like that at some point in the past. If you have the desire to do things better and learn new things, then CUE can offer a lot in both departments.&lt;/p&gt;

&lt;p&gt;P.S. I still have enough material for another blog post about CUE and YANG. I haven&amp;rsquo;t finished exploring this topic so it may be a very small article, depending on how it goes. Stay tuned for more.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Network Automation with CUE - Augmenting Ansible workflows</title>
      <link>https://networkop.co.uk/post/2022-11-cue-ansible/</link>
      <pubDate>Fri, 11 Nov 2022 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2022-11-cue-ansible/</guid>
      <description>

&lt;p&gt;Hardly any conversation about network automation that happens these days can avoid the topic of automation frameworks. Amongst the few that are still actively developed, Ansible is by far the most popular choice. Ansible ecosystem has been growing rapidly over the last few years, with modules being contributed by both internal (Redhat) and external (community) developers. Having the backing of one of the largest open-source first companies has allowed Ansible to spread into all areas of infrastructure &amp;ndash; from server automation to cloud provisioning. By following the principle of eating your own dog food, Redhat used Ansible in a lot of its own open-source projects, which made it even more popular in the masses. Another important factor in Ansible&amp;rsquo;s success is the ease of understanding. When it comes to network automation, Ansible&amp;rsquo;s stateless and agentless architecture very closely follows a standard network operation experience &amp;ndash; SSH in, enter commands line-by-line, catch any errors, save and disconnect. But like many complex software projects, Ansible is not without its own challenges, and in this post, I&amp;rsquo;ll take a look at what they are and how CUE can help overcome them.&lt;/p&gt;

&lt;h2 id=&#34;ansible-automation-workflow&#34;&gt;Ansible Automation Workflow&lt;/h2&gt;

&lt;p&gt;Let&amp;rsquo;s start with an overview of the intermediate Ansible automation workflow, that was described in the &lt;a href=&#34;https://networkop.co.uk/post/2022-10-cue-intro/&#34;&gt;previous post&lt;/a&gt;, and try to see what areas are more prone to human error or may require additional improvement. In order to do that, I&amp;rsquo;ll break it down into a sequence of steps describing how configuration data travels through this automation workflow, where it gets mutated and how it is used:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user creates a playbook, a device inventory and a set of variables describing the desired state of the network.&lt;/li&gt;
&lt;li&gt;Ansible runtime parses all input data and calculates a per-host set of variables.&lt;/li&gt;
&lt;li&gt;This set of high-level variables gets transformed into a larger set of low-level variables.&lt;/li&gt;
&lt;li&gt;The entire set of variables is now passed to a config generation module which combines them with one or more Jinja templates.&lt;/li&gt;
&lt;li&gt;The resulting semi-structured text is applied to the running device configuration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/cue-ansible.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;One of the first places where we can make a mistake is the input data. Specifically, a set of input variables is essentially a free-form YAML data structure with values sourced from up to &lt;a href=&#34;https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#understanding-variable-precedence&#34; target=&#34;_blank&#34;&gt;22 different places&lt;/a&gt;. There&amp;rsquo;s no way to verify that the shape of the input data structure is correct and the only way to validate the type of values is by using filters.&lt;/p&gt;

&lt;p&gt;However, even with filters, you can never be sure the returned value has the right type, as filters are built to &amp;ldquo;fail safe&amp;rdquo;. For example, the &lt;code&gt;ansible.utils.ipaddr&lt;/code&gt; filter will return the input value (as a string) if it&amp;rsquo;s a valid IP address, but will return a boolean &lt;code&gt;False&lt;/code&gt; if it isn&amp;rsquo;t, conflating the returned value and an error in a single variable. There&amp;rsquo;s no way to abort Ansible execution or signal to the user that the input value was incorrect unless you use &lt;code&gt;assert&lt;/code&gt; statements, which become pretty ineffective even with relatively small volumes of data.&lt;/p&gt;

&lt;p&gt;The next place where things can go wrong is the data transformation stage. This can be anything from a simple &lt;code&gt;builtin.set_fact&lt;/code&gt; module with a bunch of filters to what I describe as &amp;ldquo;Jinja programming&amp;rdquo; &amp;ndash; manipulating data structures using Jinja&amp;rsquo;s expression statements (e.g. &lt;code&gt;set&lt;/code&gt; and &lt;code&gt;do&lt;/code&gt; tags) or even building a structured document (YAML, JSON) using string interpolation. In any case, the likelihood of making a mistake gets even higher since both the input data and the transformation logic itself are dynamically-typed and Jinja is notorious for becoming &lt;a href=&#34;https://news.ycombinator.com/item?id=14777697&#34; target=&#34;_blank&#34;&gt;incomprehensible very quickly&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now we&amp;rsquo;re at the config generation phase where, once again, the input variables are passed without validation which means you can easily get tripped by one of the &lt;a href=&#34;https://docs.saltproject.io/en/latest/topics/troubleshooting/yaml_idiosyncrasies.html&#34; target=&#34;_blank&#34;&gt;YAML idiosyncrasies&lt;/a&gt; and troubleshooting Jinja templating errors is particularly painful as errors are often reported with a vague &amp;ldquo;undefined variable&amp;rdquo; message.&lt;/p&gt;

&lt;p&gt;Finally, one of the unlikely places that can benefit from CUE is the API interactions with remote devices. CUE&amp;rsquo;s &lt;a href=&#34;https://cuelang.org/docs/usecases/scripting/&#34; target=&#34;_blank&#34;&gt;scripting capabilities&lt;/a&gt; can orchestrate interaction with multiple HTTP-based APIs and, if possible, would do this concurrently. This not only accelerates execution but also reduces resource utilisation thanks to the CUE&amp;rsquo;s (Go&amp;rsquo;s) lightweight concurrency model compared to Ansible&amp;rsquo;s more expensive &lt;code&gt;os.fork()&lt;/code&gt; approach.&lt;/p&gt;

&lt;p&gt;If you go back and look at the first two areas I&amp;rsquo;ve identified above, you can see that they can easily be done by an external tool and integrated into any existing Ansible workflow without making any serious changes to how the config is generated or delivered. These will be the two things I&amp;rsquo;m going to cover in this post.&lt;/p&gt;

&lt;p&gt;The final two areas are more disruptive but may allow you to replace Ansible completely for pretty much any non-SSH API automation, i.e. JSON-RPC or REST APIs. I&amp;rsquo;ll cover them in the following article.&lt;/p&gt;

&lt;h2 id=&#34;input-data-validation&#34;&gt;Input Data Validation&lt;/h2&gt;

&lt;p&gt;If you&amp;rsquo;re thinking about giving CUE a try and now sure where to start, input data validation could be your best option. Creating a schema for input data is a good exercise to test and explore the language while having no negative impact on your automation workflow. The benefits, however, are worth it as the schema will improve your automation workflow by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validating the structural shape of input variables to catch any potential indentation errors&lt;/li&gt;
&lt;li&gt;Making sure all variables have the right type and catch any typos before you run the playbook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This could also be a good place to introduce additional constraints for values, for example, to verify that BGP ASN is within a valid range or if IP addresses are valid. In general, once you&amp;rsquo;ve started with a simple schema, you can continue mixing in more policies to tighten the range of allowed values and improve the overall data integrity.&lt;/p&gt;

&lt;p&gt;Let&amp;rsquo;s see a concrete example of how to develop a CUE schema to validate input variables using Cumulus&amp;rsquo;s &lt;code&gt;golden turtle&lt;/code&gt; &lt;a href=&#34;https://gitlab.com/cumulus-consulting/goldenturtle/cumulus_ansible_modules.git&#34; target=&#34;_blank&#34;&gt;Ansible modules&lt;/a&gt;. Get yourself a copy of this repository:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;git clone https://gitlab.com/cumulus-consulting/goldenturtle/cumulus_ansible_modules.git &amp;amp;&amp;amp; cd cumulus_ansible_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You&amp;rsquo;ll find several validated network topologies inside of the &lt;code&gt;inventories/&lt;/code&gt; directory together with a set of input variables spread across standard Ansible group and host variable directories. To make this example a bit simpler, I&amp;rsquo;ll focus on the bonds (link aggregation) configuration, and the following example shows a snippet of the &lt;code&gt;bonds&lt;/code&gt; variable from the &lt;a href=&#34;https://gitlab.com/cumulus-consulting/goldenturtle/cumulus_ansible_modules/-/blob/master/inventories/evpn_symmetric/group_vars/leaf/common.yml#L20&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;group_vars/leaf/common.yml&lt;/code&gt;&lt;/a&gt; file:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bonds:
  - name: bond1
    ports: [swp1]
    clag_id: 1
    bridge:
      access: 10
    options:
      mtu: 9000
      extras:
        - bond-lacp-bypass-allow yes
        - mstpctl-bpduguard yes
        - mstpctl-portadminedge yes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I&amp;rsquo;ve picked this example deliberately because it contains many places where we can make a mistake, but also because it can be very succinctly summarized by the following CUE schema:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;#bonds: [...{
    name: string
    ports: [...string] 
    clag_id: int
    bridge: access: int
    options: {
        mtu: int &amp;amp; &amp;lt;9999
        extras: [...string]
    }
}]

bonds: #bonds
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here we&amp;rsquo;ve created a &lt;a href=&#34;https://cuelang.org/docs/tutorials/tour/types/defs/&#34; target=&#34;_blank&#34;&gt;CUE definition&lt;/a&gt; that describes the structure and type of values expected in the &lt;code&gt;bonds&lt;/code&gt; variable. The last line &amp;ldquo;applies&amp;rdquo; the &lt;code&gt;#bonds&lt;/code&gt; schema to any existing &lt;code&gt;bonds&lt;/code&gt; variable. Assuming the above schema is saved in the &lt;code&gt;bonds.cue&lt;/code&gt; file, we can check if the input variables conform to it with the following command:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ cue vet bonds.cue inventories/evpn_symmetric/group_vars/leaf/common.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now let&amp;rsquo;s introduce a mistake by changing the value of MTU in the input variable. The resulting error message tells us exactly where the error is and why it&amp;rsquo;s not valid:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ sed -i &#39;s/mtu: 9000/mtu: 90000/&#39; inventories/evpn_symmetric/group_vars/leaf/common.yml
$ cue vet bonds.cue inventories/evpn_symmetric/group_vars/leaf/common.yml
bonds.0.options.mtu: invalid value 900000 (out of bound &amp;lt;9999):
    ./bonds.cue:8:20
    ./inventories/evpn_symmetric/group_vars/leaf/common.yml:27:13
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can experiment a bit more by changing the values in the input data, for example, try changing &lt;code&gt;ports&lt;/code&gt; to an empty list or left-shifting the indentation of &lt;code&gt;access: 10&lt;/code&gt; line.&lt;/p&gt;

&lt;p&gt;Creating schemas for every input variable can be a tedious process. However, there&amp;rsquo;s a shortcut you can take that can get you a working schema relatively easily.  It&amp;rsquo;s a two-step process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use one of the open-source code generators to produce (infer) a JSON Schema from a &lt;a href=&#34;https://www.npmjs.com/package/yaml-to-json-schema&#34; target=&#34;_blank&#34;&gt;YAML&lt;/a&gt;, &lt;a href=&#34;https://jsonschema.net/&#34; target=&#34;_blank&#34;&gt;JSON&lt;/a&gt; or a &lt;a href=&#34;https://jinja2schema.readthedocs.io/en/latest/&#34; target=&#34;_blank&#34;&gt;Jinja template&lt;/a&gt; document&lt;/li&gt;
&lt;li&gt;Convert JSON Schema to CUE using the &lt;code&gt;cue import&lt;/code&gt; command.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To make it easier to follow, I&amp;rsquo;ve run through the original &lt;code&gt;bonds&lt;/code&gt; variable through an &lt;a href=&#34;https://jsonformatter.org/yaml-to-jsonschema&#34; target=&#34;_blank&#34;&gt;online converter&lt;/a&gt;, saved the result in a &lt;code&gt;schema.json&lt;/code&gt; file, and imported it using the &lt;code&gt;cue import -f -p schema schema.json&lt;/code&gt; command. The resulting &lt;code&gt;schema.cue&lt;/code&gt; file contained the following:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;bonds: [...#Bond]

#Bond: {
        name: string
        ports: [...string]
        clag_id: int
        bridge:  #Bridge
        options: #Options
}

#Bridge: access: int

#Options: {
        mtu: int
        extras: [...string]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Although it&amp;rsquo;s a slightly different (more verbose) version of my hand-written CUE schema, most of the values are exactly the same. The only bits that are missing are constraints and policies, which are optional and can be added at a later stage. You can find another example of the above process on the &lt;a href=&#34;https://github.com/networkop/cue-ansible/tree/main/jinja&#34; target=&#34;_blank&#34;&gt;Jinja to CUE&lt;/a&gt; page of my &lt;a href=&#34;https://github.com/networkop/cue-ansible&#34; target=&#34;_blank&#34;&gt;cue-ansible repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once you have your schemas developed, you can start adding them to an existing Ansible workflow. Here are some ideas of how this can be done, starting from the easiest one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You can add an extra task to the top of your Ansible playbook that uses &lt;code&gt;shell&lt;/code&gt; module to execute &lt;code&gt;cue vet&lt;/code&gt; against input variables.&lt;/li&gt;
&lt;li&gt;If you have an existing CI system, you can add the &lt;code&gt;cue vet&lt;/code&gt; as a new step before &lt;code&gt;ansible-playbook&lt;/code&gt; command is executed.&lt;/li&gt;
&lt;li&gt;Another option is to create a custom module that can be configured to run CUE schema validation for any schema or input variables.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last option requires you to write an Ansible module in Go, but it allows you to have a native way of providing inputs and consuming outputs:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- name: Validate input data model with CUE
  cue_validate:
    schema: &amp;quot;schemas/input.cue&amp;quot;
    input: &amp;quot;{{ hostvars[inventory_hostname] | string | b64encode }}&amp;quot;
  delegate_to: localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can find a &lt;a href=&#34;https://github.com/networkop/cue-ansible/blob/main/validation/src/main.go&#34; target=&#34;_blank&#34;&gt;reference implementation&lt;/a&gt; of this module with an example workflow in the &lt;a href=&#34;https://github.com/networkop/cue-ansible/tree/main/validation&#34; target=&#34;_blank&#34;&gt;Validation&lt;/a&gt; page of my &lt;a href=&#34;https://github.com/networkop/cue-ansible&#34; target=&#34;_blank&#34;&gt;cue-ansible repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&#34;data-transformation&#34;&gt;Data Transformation&lt;/h2&gt;

&lt;p&gt;At this point, we&amp;rsquo;ve only used CUE for schema validation. The next logical step is to ingest all input values in CUE and start working with them as native CUE values. There are many benefits to using CUE for value management, and I&amp;rsquo;ll cover some of them in the following blog posts, but for now, let me focus on a very common task of data transformation.&lt;/p&gt;

&lt;p&gt;For demonstration purposes, I&amp;rsquo;ll be using Arista&amp;rsquo;s Validated Design (&lt;a href=&#34;https://github.com/aristanetworks/ansible-avd&#34; target=&#34;_blank&#34;&gt;AVD&lt;/a&gt;) as it&amp;rsquo;s one of the most interesting examples of data transformation done in Ansible. AVD uses a combination of custom Python modules and Jinja templates to transform high-level input data and generate structured configs that have all the values required by devices. My goal would be to demonstrate CUE&amp;rsquo;s data transformation capabilities by removing parts of Ansible code and Jinja templates and replacing them with CUE code, while keeping both inputs and outputs unchanged.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/arista-avd.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Let&amp;rsquo;s start by cloning the AVD repo and pinning the Ansible collection path to that directory.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ git clone https://github.com/aristanetworks/ansible-avd.git &amp;amp;&amp;amp; cd ansible-avd
$ export ANSIBLE_COLLECTIONS_PATH=$(pwd)
$ export OUT_DIR=intended/structured_configs
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Using one of the included example topologies, I run through the entire data transformation stage shown in the above diagram, first without using CUE.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ cd ansible_collections/arista/avd/examples/l2ls-fabric
$ ansible-playbook build.yml  --tags build,facts,debug
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In the &lt;code&gt;./intended/structured_configs&lt;/code&gt; directory, I now have a set of structured device configs and input host variables. Next, I&amp;rsquo;m going to do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Import all input host variables to allow me to use them natively as CUE values.&lt;/li&gt;
&lt;li&gt;Save the generated structured device configuration of &lt;code&gt;LEAF1&lt;/code&gt; switch as a baseline for future comparison (I&amp;rsquo;m running it through &lt;code&gt;cue eval --out=yaml&lt;/code&gt; simply to update the indentation).&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ cue import -p hostvars -f $OUT_DIR/LEAF1-debug-vars.yml
$ mv $OUT_DIR/LEAF1-debug-vars.cue leaf1.cue
$ cue eval $OUT_DIR/LEAF1.yml --out=yaml &amp;gt; $OUT_DIR/LEAF1.base.yml   
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In order to keep the input values separate from the data transformation logic, I&amp;rsquo;ve moved them into their own &lt;code&gt;hostvars&lt;/code&gt; package using the &lt;code&gt;-p&lt;/code&gt; flag in the command above. CUE&amp;rsquo;s code organisation practices are very similar to Go&amp;rsquo;s (programming language) and allow me to group code into packages and group similar packages into modules. In order to import the &lt;code&gt;hostvars&lt;/code&gt; package, I first need to initialise a CUE module:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;cue mod init arista.avd
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now I can create a new file called &lt;code&gt;transform.cue&lt;/code&gt; and get access to all input variables using the &lt;code&gt;arista.avd:hostvars&lt;/code&gt; import statement. From here on, I can use a standard set of data manipulation techniques like the &lt;code&gt;for&lt;/code&gt; loop, string interpolation, variable declarations and conditionals to expand the high-level data model into a low-level structured configuration, focusing only on port channel interfaces for this example:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;package avd

import (
	&amp;quot;arista.avd:hostvars&amp;quot;
	&amp;quot;strconv&amp;quot;
)

// Uplink port channels
port_channel_interfaces: {
	for link in hostvars.switch.uplinks if link.channel_group_id != _|_ {
		let groupID = strconv.Atoi(link.channel_group_id)

		&amp;quot;Port-Channel\(groupID)&amp;quot;: {
			description: link.channel_description + &amp;quot;_Po\(groupID)&amp;quot;
			type:        &amp;quot;switched&amp;quot;
			shutdown:    false
			if link.vlans != _|_ {
				vlans: link.vlans
			}
			mode: &amp;quot;trunk&amp;quot;
			if hostvars.switch.mlag != _|_ {
				mlag: groupID
			}
		}
	}
}

// MLAG port channels
if hostvars.switch.mlag != _|_ {
    port_channel_interfaces: {
        let groupID = strconv.Atoi(hostvars.switch.mlag_port_channel_id)

        &amp;quot;Port-Channel\(groupID)&amp;quot;: {
            description: &amp;quot;MLAG_PEER_&amp;quot; + hostvars.switch.mlag_peer + &amp;quot;_Po\(groupID)&amp;quot;
            type: &amp;quot;switched&amp;quot;
            shutdown: false,
            vlans: hostvars.switch.mlag_peer_link_allowed_vlans
            mode: &amp;quot;trunk&amp;quot;,
            trunk_groups: [&amp;quot;MLAG&amp;quot;]
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;if value != _|_&lt;/code&gt; expression in the above example is a check if a value is defined, where &lt;code&gt;_|_&lt;/code&gt; is a special &lt;a href=&#34;https://cuelang.org/docs/tutorials/tour/types/bottom/&#34; target=&#34;_blank&#34;&gt;&amp;ldquo;bottom&amp;rdquo; or error value&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The example above contains enough data transformation logic to generate the required set of port-channel interfaces, and can be checked as follows:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ cue eval transform.cue
port_channel_interfaces: {
    &amp;quot;Port-Channel47&amp;quot;: {
        description: &amp;quot;MLAG_PEER_LEAF2_Po47&amp;quot;
        type:        &amp;quot;switched&amp;quot;
        shutdown:    false
        vlans:       &amp;quot;2-4094&amp;quot;
        mode:        &amp;quot;trunk&amp;quot;
        trunk_groups: [&amp;quot;MLAG&amp;quot;]
    }
    &amp;quot;Port-Channel1&amp;quot;: {
        description: &amp;quot;SPINES_Po1&amp;quot;
        type:        &amp;quot;switched&amp;quot;
        shutdown:    false
        vlans:       &amp;quot;10,20&amp;quot;
        mlag:        1
        mode:        &amp;quot;trunk&amp;quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now let&amp;rsquo;s remove the port channel data generation logic from AVD&amp;rsquo;s Python module and completely wipe out a corresponding Jinja template:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ sed -i &#39;/port_channel_interface_name: port_channel_interface,/d&#39; ../../roles/eos_designs/python_modules/mlag/__init__.py
$ cat /dev/null &amp;gt; ../../roles/eos_designs/templates/underlay/interfaces/port-channel-interfaces.j2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I re-run the playbook again to see what results I get after the above changes:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ ansible-playbook build.yml  --tags build,facts,debug
$ cue eval $OUT_DIR/LEAF1.yml --out=yaml &amp;gt; $OUT_DIR/LEAF1.new.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The resulting structured config should contain no port channel configuration data, which I verify by comparing with the baseline:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;$ diff $OUT_DIR/LEAF1.new.yml $OUT_DIR/LEAF1.base.yml
67c67,82
&amp;lt; port_channel_interfaces: {}
---
&amp;gt; port_channel_interfaces:
&amp;gt;   Port-Channel47:
&amp;gt;     description: MLAG_PEER_LEAF2_Po47
&amp;gt;     type: switched
&amp;gt;     shutdown: false
&amp;gt;     vlans: &amp;quot;2-4094&amp;quot;
&amp;gt;     mode: trunk
&amp;gt;     trunk_groups:
&amp;gt;       - MLAG
&amp;gt;   Port-Channel1:
&amp;gt;     description: SPINES_Po1
&amp;gt;     type: switched
&amp;gt;     shutdown: false
&amp;gt;     vlans: 10,20
&amp;gt;     mode: trunk
&amp;gt;     mlag: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;However, since I already have the correct port channel data produced by my CUE code, I can merge it with the latest structured config. Note that I pass both CUE and YAML files as the input to the &lt;code&gt;cue eval&lt;/code&gt; command, leaving it up to CUE to recognise the type, import and evaluate everything as a single set of CUE values.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ cue eval transform.cue $OUT_DIR/LEAF1.yml --out=yaml &amp;gt; $OUT_DIR/LEAF1.new.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Re-running the earlier diff command should show that the new structured device config looks exactly the same as the baseline (with a minor exception of struct field re-ordering). This means I have generated the same exact output from the same set of inputs, bypassing Python and Jinja and moving all port-channel data transformation logic into CUE. This way I have consolidated and unified data transformation and made it easier to read and reason about.&lt;/p&gt;

&lt;p&gt;Now that I&amp;rsquo;ve covered the first two stages of the advanced automation workflow, it&amp;rsquo;s time to move on to the final two stages and wrap up the Ansible portion of this blog post series. In the next post, I&amp;rsquo;ll show how to hierarchically organise CUE code to minimise boilerplate, how to work with externally-sourced data like IPAM or secret stores and use CUE&amp;rsquo;s scripting to apply configurations to multiple devices at the same time.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Network Automation with CUE - Introduction</title>
      <link>https://networkop.co.uk/post/2022-10-cue-intro/</link>
      <pubDate>Thu, 27 Oct 2022 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2022-10-cue-intro/</guid>
      <description>

&lt;p&gt;In the past few years, network automation has made its way from a new and fancy way of configuring devices to a well-recognized industry practice. What started as a series of &amp;ldquo;hello world&amp;rdquo; examples has evolved into an entire discipline with books, professional certifications and dedicated career paths. It&amp;rsquo;s safe to say that today, most large-scale networks (&amp;gt;100 devices) are at least deployed (day 0) and sometimes managed (day 1+) using an automated workflow. However, at the heart of these workflows are the same exact principles and tools that were used in the early days. Of course, these tools have evolved and matured but they still have the same scope and limitations. Very often, these limitations are only becoming obvious once we hit a certain scale or complexity, which makes it even more difficult to replace them. The easiest option is to accept and work around them, forcing the square peg down the round hole. In this post, I&amp;rsquo;d like to propose an alternative approach to what I&amp;rsquo;d consider &amp;ldquo;traditional&amp;rdquo; network automation practices by shifting the focus from &amp;ldquo;driving the CLI&amp;rdquo; to the management of data. I believe that this adjustment will enable us to build automation workflows that are much more robust and scalable and there are emerging tools and practices that were designed to address exactly that.&lt;/p&gt;

&lt;h2 id=&#34;evolution-of-network-configuration-management&#34;&gt;Evolution of Network Configuration Management&lt;/h2&gt;

&lt;p&gt;In order to understand why data management is important, we need to have a closer look at what constitutes a typical network automation workflow. The most basic process starts by combining a device data model, represented by a free-form data structure (e.g. YAML), with a text template (e.g. Jinja) to produce the desired device configuration. This configuration is then passed to a function that implements the underlying transport protocol (e.g. netmiko), which applies the desired changes. Some of these steps can be abstracted away by automation frameworks (e.g. Ansible) but largely the process still looks the same under the hood. This is what you can see on the left-hand side of the following diagram:&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/cue-evolution.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;While the basic workflow may work well for the initial network configuration, it is rarely suitable for ongoing operations due to its inherent verbosity. The natural reaction to that is to create another layer of abstraction that hides common design conventions, configuration defaults and computable attributes behind a terse high-level data model, as depicted by the &amp;ldquo;intermediate workflow&amp;rdquo; in the above diagram. This high-level data model simplifies the end-user experience of interacting with the automation workflow, but it comes at the expense of additional complexity hidden in the high-to-low-level translation logic.&lt;/p&gt;

&lt;p&gt;Finally, some physical networks have decided to replicate the self-service cloud experience by allowing some parts of their state to be managed dynamically. One simple example is to allow a compute team to manage the VLAN assignment on the downlink network ports. This meant that a single, flat-text data structure is no longer enough to store the high-level configuration intent, and we split it across multiple (preferably) non-overlapping sources of truth, visualized by the &amp;ldquo;advanced workflow&amp;rdquo; in the above diagram.&lt;/p&gt;

&lt;p&gt;If you look at the above diagram, you might notice one theme that emerges and evolves together with the complexity of automation workflows. It is the ever-increasing focus on data. Thanks to the growing number of templates, we started caring less about individual vendor configuration dialects and more about how to source, structure and combine input configuration values. I would argue that these input values have become the new API, since the old APIs (&amp;ldquo;industry-standard&amp;rdquo; CLI) were not built for automation and eventually got abstracted away by libraries like scrapli or netmiko and a ton of (mostly) Jinja templates.&lt;/p&gt;

&lt;p&gt;The same argument can be applied to the YANG-based APIs, which &lt;em&gt;were&lt;/em&gt; designed for machine-to-machine communication and are slowly but steadily getting more traction. Those APIs are often abstracted away by software platforms, such as OpenDaylight or Tail-f NSO, or libraries like &lt;a href=&#34;https://github.com/openconfig/ygot&#34; target=&#34;_blank&#34;&gt;ygot&lt;/a&gt;, and operational tasks are, once again, reduced to the management of input data.&lt;/p&gt;

&lt;h2 id=&#34;automation-tools&#34;&gt;Automation Tools&lt;/h2&gt;

&lt;p&gt;I want to frame the discussion of automation tools in the context of the &lt;a href=&#34;http://mikehadlow.blogspot.com/2012/05/configuration-complexity-clock.html&#34; target=&#34;_blank&#34;&gt;configuration complexity clock&lt;/a&gt;. The main premise of this theory is that the process of finding the right level of abstraction for configuration values is cyclical. Here&amp;rsquo;s my free interpretation of the original story, translated into the network automation reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;00:00&lt;/strong&gt;: We need to configure a network but don&amp;rsquo;t have time for proper planning, so we have all configurable values hard-coded in flat-text configuration files and simply push them to network devices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;03:00&lt;/strong&gt;: We realise that some parts of the network need to change, so we extract some of the hard-coded values, simplify them and make them configurable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;06:00&lt;/strong&gt;: The size of the configurable values continues to grow and we start building guardrails to prevent typical configuration mistakes and guarantee value uniqueness across the environment. We create a schema to validate input data and may even expose it via a GUI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;09:00&lt;/strong&gt;: At some point the guard rails, schemas and policy engines start being a hurdle and we decide to consolidate all of them in a single framework, driven by its own DSL. Quickly realising that the framework can not meet all our requirements, we start extending it with custom code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;12:00&lt;/strong&gt;: Now we have all our policies embedded in custom framework extensions and values hard-coded in the DSL, the network management process looks not much different from where we started.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Strictly speaking, this is more of a fable than a theory. It shows what a constant strive for improvement can do to an application&amp;rsquo;s user interface. If you read the original article, the author says that very few organisations go all the way around the clock, which means the majority settle somewhere in between. If we look at the current state of network automation, we can see a confirmation of that &amp;ndash; &lt;em&gt;majority&lt;/em&gt; of the network operators settle on one of the following two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Everything is done with a DSL (Ansible + Jinja)&lt;/li&gt;
&lt;li&gt;Everything is done with a general-purpose programming language (Python)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Similar to the choice of their preferred hardware vendor, the choice between the above two options can be almost religious to some people. There are engineers who wouldn&amp;rsquo;t want to come close to Ansible and there are those who shove all the logic into Ansible DSL, ignoring the exponentially-increasing complexity. The most important point is that both groups seem to have settled on their choice and accepted the caveats and limitations resulting from their decisions. So far, I have not seen any attempts to upset this status quo by exploring and explaining alternative options.&lt;/p&gt;

&lt;p&gt;What if there was another option that would allow us to write a true statically-typed code instead of using type hints, avoid &lt;a href=&#34;https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#understanding-variable-precedence&#34; target=&#34;_blank&#34;&gt;variable override&lt;/a&gt; hell, with built-in low-cost concurrency and task orchestration? What if we could use a tool that was purposefully built to transform and generate configuration data instead of engaging in Jinja programming with Ansible (that &lt;a href=&#34;https://twitter.com/privateip/status/1174410756181413889&#34; target=&#34;_blank&#34;&gt;was never designed&lt;/a&gt; for this) or trying to write &lt;a href=&#34;https://twitter.com/markdalgleish/status/1554930570844848128&#34; target=&#34;_blank&#34;&gt;error-proof&lt;/a&gt; and &lt;a href=&#34;https://twitter.com/dbarrosop/status/1397161258990903298&#34; target=&#34;_blank&#34;&gt;readable&lt;/a&gt; Python?&lt;/p&gt;

&lt;p&gt;The author of the configuration complexity clock article cautions us against making rash decisions (especially from rolling your own DSL) and also suggests that at a low-enough scale simpler solutions may be the best option. I would agree with him. If you think that you get enough out of your current automation solution &amp;ndash; you don&amp;rsquo;t feel like you&amp;rsquo;re swimming against the tide all the time and you&amp;rsquo;re confident that when you move on, the next person will be able to pick up and continue your work without re-write everything from scratch &amp;ndash; then you don&amp;rsquo;t need to change. However, I&amp;rsquo;d like to show you that you can do better. You can create a solution that is faster, more robust to failures and easier to understand and extend. Like with anything new, there&amp;rsquo;s a price you have to pay, by learning and changing your automation workflows, but the ultimate benefit may very well be worth it.&lt;/p&gt;

&lt;h2 id=&#34;introducing-cue&#34;&gt;Introducing CUE&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://cuelang.org/&#34; target=&#34;_blank&#34;&gt;CUE&lt;/a&gt; or cuelang was built to manage configuration data which, as we&amp;rsquo;ve seen above, is one of the most critical parts of advanced network automation workflows. CUE tries to strike a balance between the simplicity of a DSL and the efficiency of a general-purpose programming language. Visually, it looks very similar to JSON (it is a superset of JSON) with a relaxed grammar, e.g. you can leave comments and omit string quotes for field names. This is an example of a CUE syntax that defines a set of BGP configuration values:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bgp: {
  asn: 65123
  router_id: &amp;quot;192.0.2.1&amp;quot;
  neighbors: {
    swp51: {
      unnumbered: true
      remote_as:  &amp;quot;external&amp;quot;
    }
    swp52: {
      unnumbered: true
      remote_as:  &amp;quot;external&amp;quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The main idea is that you write all your configuration values, constraints and code generation rules in CUE code. It becomes your new source of truth and can later output values in YAML or JSON format, which you can either pass to a text template (e.g. Jinja) to generate a semi-structured configuration or send as-is to a remote device (in case it supports structured input).&lt;/p&gt;

&lt;p&gt;One of the two strongest qualities of CUE for network automation workflows (in my opinion) is &lt;strong&gt;static data typing&lt;/strong&gt;. While we can work with the free-form data defined above, we can easily create a simple schema that would ensure that both the shape of the &lt;code&gt;bgp&lt;/code&gt; struct and the type of all its values are exactly what we&amp;rsquo;d expect. Here&amp;rsquo;s the most straightforward way of doing this &amp;ndash; we define another data structure with the same name, CUE will unify them and validate the values above are correct:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bgp: {
  asn:       int
  router_id: net.IPv4 &amp;amp; string
  neighbors: [=~&amp;quot;^swp&amp;quot;]: {
    unnumbered: bool | *true
    remote_as:  int | &amp;quot;external&amp;quot; | &amp;quot;internal&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In the above example, we mix static typing (&lt;code&gt;asn&lt;/code&gt; value must be an integer) with constraints (&lt;code&gt;router_id&lt;/code&gt; is a string that is also a valid IPv4 address), defaults (default value for &lt;code&gt;unnumbered&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;) and regex matching (only apply the constraints and defaults to neighbors starting with &lt;code&gt;swp&lt;/code&gt;). Now we can safely add or remove additional types and constraints as our data evolves, relying on CUE to produce the correct configuration values.&lt;/p&gt;

&lt;p&gt;Another big selling point of CUE is its powerful &lt;strong&gt;data templating and generation&lt;/strong&gt; capabilities. CUE natively supports value interpolation, conditional fields and &lt;code&gt;for&lt;/code&gt; loops which allow us to generate larger data sets from smaller, more concise inputs. In addition, you can import helper packages from CUE&amp;rsquo;s &lt;a href=&#34;https://cuetorials.com/overview/standard-library/&#34; target=&#34;_blank&#34;&gt;standard library&lt;/a&gt; to perform common data operations. The following contrived example demonstrates the use of field comprehension (the &lt;code&gt;for&lt;/code&gt; loop), local variables (the &lt;code&gt;let&lt;/code&gt; keyword), conditionals and two helper packages from the standard library:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;import (
  &amp;quot;strings&amp;quot;
  &amp;quot;strconv&amp;quot;
)

uplinks: [&amp;quot;swp53&amp;quot;, &amp;quot;swp54&amp;quot;]

bgp: neighbors: {
  for uplink in uplinks {
    let parts = strings.SplitAfter(uplink, &amp;quot;swp&amp;quot;)

    if len(parts) &amp;gt; 1 {
      let intfNum = strconv.ParseInt(parts[1], 10, 32)

      if intfNum &amp;gt;= 50 {
        &amp;quot;\(uplink)&amp;quot;: {
          remote_as: &amp;quot;external&amp;quot;
        }
      }
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can find the above code examples in the CUE playground (&lt;a href=&#34;https://cuelang.org/play/?id=Cn_VFwc2oZb#cue@export@cue&#34; target=&#34;_blank&#34;&gt;link&lt;/a&gt;) and experiment by changing the values and observing the result, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change the &lt;code&gt;asn&lt;/code&gt; field to a string instead of an integer&lt;/li&gt;
&lt;li&gt;Try adding a couple of new values to the &lt;code&gt;uplink&lt;/code&gt; list, e.g. &lt;code&gt;swp50&lt;/code&gt;, &lt;code&gt;swp49&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Change the &lt;code&gt;router_id&lt;/code&gt; field to contain an invalid IPv4 address&lt;/li&gt;
&lt;li&gt;In the drop-down menu at the top of the page, change the output to JSON or YAML&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the examples above, we&amp;rsquo;re just scratching the surface of what CUE is capable of. Things I haven&amp;rsquo;t covered here include module packaging, integration with OpenAPI, YAML, JSON and Go, and the built-in support for external network calls. The goal of the current article is mainly to whet your appetite but I&amp;rsquo;ll try to cover these and other interesting features in the following blog posts. Here&amp;rsquo;s what you can expect to find about in the upcoming material:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Augment your existing Ansible-based automation workflow with CUE&lt;/li&gt;
&lt;li&gt;How to use CUE for YANG-based APIs&lt;/li&gt;
&lt;li&gt;Orchestrate API interactions with remote devices&lt;/li&gt;
&lt;li&gt;Reducing configuration boilerplate&lt;/li&gt;
&lt;li&gt;Performance comparison of CUE vs Ansible&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Containerising NVIDIA Cumulus Linux</title>
      <link>https://networkop.co.uk/post/2021-05-cumulus-ignite/</link>
      <pubDate>Tue, 25 May 2021 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2021-05-cumulus-ignite/</guid>
      <description>

&lt;p&gt;In one of his &lt;a href=&#34;https://blog.ipspace.net/2021/04/katacoda-netsim-containerlab-frr.html?utm_source=atom_feed&#34; target=&#34;_blank&#34;&gt;recent posts&lt;/a&gt;, Ivan raises a question: &amp;ldquo;I can’t grasp why Cumulus releases a Vagrant box, but not a Docker container&amp;rdquo;. Coincidentally, only a few weeks before that I had &lt;a href=&#34;https://twitter.com/networkop1/status/1384175045950414848&#34; target=&#34;_blank&#34;&gt;managed&lt;/a&gt; to create a Cumulus Linux container image. Since then, I&amp;rsquo;ve done a lot of testing and discovered limitations of the pure containerised approach and how to overcome them while still retaining the container user experience. This post is a documentation of my journey from the early days of running Cumulus on Docker to the integration with containerlab and, finally, running Cumulus in microVMs backed by AWS&amp;rsquo;s Firecracker and Weavework&amp;rsquo;s Ignite.&lt;/p&gt;

&lt;h2 id=&#34;innovation-trigger&#34;&gt;Innovation Trigger&lt;/h2&gt;

&lt;p&gt;One of the main reason for running containerised infrastructure is the famous Docker UX. Containers existed for a very long time but they only became mainstream when docker released their container engine. The simplicity of a typical docker workflow (build, ship, run) made it accessible to a large number of not-so-technical users and was the key to its popularity.&lt;/p&gt;

&lt;p&gt;Virtualised infrastructure, including networking operating systems, has mainly been distributed in a VM form-factor, retaining much of the look and feel of the real hardware for the software processes running on top. However it didn&amp;rsquo;t stop people from looking for a better and easier way to run and test it, some of the smartest people in the industry are always &lt;a href=&#34;https://twitter.com/ibuildthecloud/status/1362162684637061121&#34; target=&#34;_blank&#34;&gt;looking&lt;/a&gt; for an alternative to a traditional Libvirt/Vagrant experience.&lt;/p&gt;

&lt;p&gt;While VM tooling has been pretty much stagnant for the last decade (think Vagrant), containers have amassed a huge ecosystem of tools and an active community around it. Specifically in the networking area, in the last few years we&amp;rsquo;ve seen commercial companies like &lt;a href=&#34;https://www.fastly.com/press/press-releases/fastly-achieves-100-tbps-edge-capacity-milestone&#34; target=&#34;_blank&#34;&gt;Tesuto&lt;/a&gt; and multiple open-source projects like &lt;a href=&#34;https://github.com/plajjan/vrnetlab&#34; target=&#34;_blank&#34;&gt;vrnetlab&lt;/a&gt;, &lt;a href=&#34;https://github.com/networkop/docker-topo&#34; target=&#34;_blank&#34;&gt;docker-topo&lt;/a&gt;, &lt;a href=&#34;https://github.com/networkop/k8s-topo&#34; target=&#34;_blank&#34;&gt;k8s-topo&lt;/a&gt; and, most recently &lt;a href=&#34;https://containerlab.srlinux.dev/&#34; target=&#34;_blank&#34;&gt;containerlab&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So when I joined Nvidia in April 2021, I thought it&amp;rsquo;d be a fun experiment for me to try to containerise Cumulus Linux and learn how the operating system works in the process.&lt;/p&gt;

&lt;h2 id=&#34;peak-of-inflated-expectations&#34;&gt;Peak of Inflated Expectations&lt;/h2&gt;

&lt;p&gt;Building a container image was the first and, as it turned out, the easiest problem to solve. Thanks to the Debian-based architecture of Cumulus Linux, I was able to build a complete container image with just a few lines:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-Dockerfile&#34;&gt;FROM debian:buster

COPY data/packages packages
COPY data/sources.list /etc/apt/sources.list
COPY data/trusted.gpg /etc/apt/trusted.gpg
RUN apt install --allow-downgrades -y $(cat packages)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I extracted the list of installed packages and public APT repos from an existing Cumulus VX VM, copied them into a base &lt;code&gt;debian:buster&lt;/code&gt; image and ran &lt;code&gt;apt install&lt;/code&gt; &amp;ndash; that&amp;rsquo;s how easy it was. Obviously, the &lt;a href=&#34;https://github.com/networkop/cx/blob/main/Dockerfile&#34; target=&#34;_blank&#34;&gt;actual Dockerfile&lt;/a&gt; ended up being a lot longer, but the main work is done in just these 5 lines. The rest of the steps are just setting up the required 3rd party packages and implement various workarounds and hacks. Below is a simplified view of the resulting Cumulus image:&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/cumulus-cx.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Once the image is built, it can be run with just a single command. Note the presence of  the &lt;code&gt;privileged&lt;/code&gt; flag, which is the easiest way to run systemd and provide NET_ADMIN and other capabilities required by Cumulus daemons:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker run -d --name cumulus --privileged networkop/cx:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A few seconds later, the entire Cumulus software stack is fully initialised and ready for action. Users can either start an interactive session or run ad-hoc commands to communicate with Cumulus daemons:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ docker exec cumulus net show system
Hostname......... 5b870d5c3d31
Build............ Cumulus Linux 4.3.0
Uptime........... 13 days, 5:03:30.690000

Model............ Cumulus VX
Memory........... 12GB
Disk............. 256GB
Vendor Name...... Cumulus Networks
Part Number...... 4.3.0
Base MAC Address. 02:42:C0:A8:DF:02
Serial Number.... 02:42:C0:A8:DF:02
Product Name..... Containerised VX
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;All this seemed pretty cool but I still had doubts over the functionality of Cumulus dataplane on a general-purpose kernel. Most of the traditional networking vendors do not rely on native kernel dataplane and heavily modify or bypass it completely in order to implement all of the required NOS features. My secret hope was that Cumulus, being the Linux-native NOS, would somehow make it work with just a standard set of kernel features. The only way to find this out was to test.&lt;/p&gt;

&lt;h2 id=&#34;building-a-test-lab&#34;&gt;Building a test lab&lt;/h2&gt;

&lt;p&gt;I&amp;rsquo;ve decided that the best way to test is to re-implement the &lt;a href=&#34;https://gitlab.com/cumulus-consulting/goldenturtle/cldemo2&#34; target=&#34;_blank&#34;&gt;Cumulus Test Drive&lt;/a&gt; environment to make use of Ansible playbooks that come with it. Here&amp;rsquo;s a short snippet of containerlab&amp;rsquo;s topology definition matching the CTD&amp;rsquo;s &lt;a href=&#34;https://gitlab.com/cumulus-consulting/goldenturtle/cldemo2/-/blob/master/documentation/diagrams/cldemo-pod.png&#34; target=&#34;_blank&#34;&gt;topology&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;name: cldemo2-mini

topology:
  nodes:
    leaf01:
      kind: linux
      image: networkop/cx:4.3.0
    leaf02:
      kind: linux
      image: networkop/cx:4.3.0
...

  links:
    - endpoints: [&amp;quot;leaf01:swp1&amp;quot;, &amp;quot;server01:eth1&amp;quot;]
    - endpoints: [&amp;quot;leaf01:swp2&amp;quot;, &amp;quot;server02:eth1&amp;quot;]
    - endpoints: [&amp;quot;leaf01:swp3&amp;quot;, &amp;quot;server03:eth1&amp;quot;]
    - endpoints: [&amp;quot;leaf02:swp1&amp;quot;, &amp;quot;server01:eth2&amp;quot;]
    - endpoints: [&amp;quot;leaf02:swp2&amp;quot;, &amp;quot;server02:eth2&amp;quot;]
    - endpoints: [&amp;quot;leaf02:swp3&amp;quot;, &amp;quot;server03:eth2&amp;quot;]
    - endpoints: [&amp;quot;leaf01:swp49&amp;quot;, &amp;quot;leaf02:swp49&amp;quot;]
    - endpoints: [&amp;quot;leaf01:swp50&amp;quot;, &amp;quot;leaf02:swp50&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The entire lab can be spun up with a single command in under 20 seconds (on a 10th gen i7 in WSL2):&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ sudo containerlab deploy -t cldemo2.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At the end of the &lt;code&gt;deploy&lt;/code&gt; action, containerlab generates an Ansible inventory file which, with a few minor modifications, can be re-used for the Cumulus Ansible &lt;a href=&#34;https://gitlab.com/cumulus-consulting/goldenturtle/cumulus_ansible_modules&#34; target=&#34;_blank&#34;&gt;modules&lt;/a&gt;. At this stage, I was able to test any of the 4 available EVPN-based &lt;a href=&#34;https://gitlab.com/cumulus-consulting/goldenturtle/cumulus_ansible_modules#how-to-use&#34; target=&#34;_blank&#34;&gt;designs&lt;/a&gt;, swap them around with just a few commands and it all had taken me just a few hours to build. This is where my luck has run out&amp;hellip;&lt;/p&gt;

&lt;h2 id=&#34;the-trough-of-disillusionment&#34;&gt;The Trough of Disillusionment&lt;/h2&gt;

&lt;p&gt;The first few topologies I&amp;rsquo;d spun up and tested worked pretty well out of the box, however I did notice that my fans were spinning like crazy. Upon further examination, I had noticed that the &lt;code&gt;clagd&lt;/code&gt; (MLAG daemon) and &lt;code&gt;neighmgrd&lt;/code&gt; (ARP watchdog) were intermittently fighting to take over all available CPU threads while nothing was showing up in the logs. That&amp;rsquo;s when I decided to have a look at the peerlink, thankfully it was super easy to do &lt;code&gt;ip netns exec FOO tcpdump&lt;/code&gt; from my WSL2 VM. When I saw hundreds of lines flying on my screen in the next few seconds, I realised it was a L2 loop (it turned out all of the packets were ARP).&lt;/p&gt;

&lt;p&gt;At this point, it is worth mentioning that one of the hacks/workarounds I had to implement when building the image was stubbing out the &lt;code&gt;mstpd&lt;/code&gt; (it wasn&amp;rsquo;t able to take over the bridge&amp;rsquo;s STP control plane). At first, I didn&amp;rsquo;t think too much of it &amp;ndash; kernel was still running CSTP and the speed of convergence wasn&amp;rsquo;t that big of an issue for me. However, as I was digging deeper, I realised that &lt;code&gt;clagd&lt;/code&gt; must be communicating with &lt;code&gt;mstpd&lt;/code&gt; in order to control the state of the peerlink VLAN interfaces (traffic is never forwarded over the peerlink under normal conditions). That fact alone meant that neither the standard kernel STP implementation nor &lt;a href=&#34;https://github.com/mstpd/mstpd&#34; target=&#34;_blank&#34;&gt;upstream mstpd&lt;/a&gt; would ever be able to cooperate with &lt;code&gt;clagd&lt;/code&gt; &amp;ndash; there&amp;rsquo;s no standard for MLAG (although I suspect most implementations are written by the same set of people). My heart sank, at this stage I was ready to give up and admit that there&amp;rsquo;s no way that one of the most widely deployed features (MLAG) would work inside a container.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It turned out that CL&amp;rsquo;s version of &lt;code&gt;mstpd&lt;/code&gt; is different from the one upstream and relies on a custom &lt;code&gt;bridge&lt;/code&gt; kernel module in order to function properly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, there was &lt;em&gt;one&lt;/em&gt; way to make Cumulus Linux work in a containerised environment and that would be to run it over a native Cumulus Kernel which, as I discovered later, was very &lt;a href=&#34;http://oss.cumulusnetworks.com/CumulusLinux-2.5.1/patches/kernel/&#34; target=&#34;_blank&#34;&gt;heavily patched&lt;/a&gt;. So, in theory, I could run tests on a beefy Cumulus VX VM with all services but docker turned off but that would be a big ask and not a nice UX I was hoping for&amp;hellip;&lt;/p&gt;

&lt;h2 id=&#34;slope-of-enlightenment&#34;&gt;Slope of Enlightenment&lt;/h2&gt;

&lt;p&gt;This is when I thought about the &lt;a href=&#34;https://firecracker-microvm.github.io/&#34; target=&#34;_blank&#34;&gt;Firecracker&lt;/a&gt; &amp;ndash; the lightweight VM manager released by AWS to run Lambda and Fargate services (&lt;a href=&#34;https://github.com/firecracker-microvm/firecracker/blob/main/CREDITS.md&#34; target=&#34;_blank&#34;&gt;originally&lt;/a&gt; based on the work of the Chromium OS team). I&amp;rsquo;d started looking at the potential candidates for FC VM orchestration and got very excited when I saw both &lt;a href=&#34;https://github.com/firecracker-microvm/firecracker-containerd/blob/f320d3636aee41661eb525b284ce6213f6c7a3d5/docs/networking.md&#34; target=&#34;_blank&#34;&gt;firecracker-containerd&lt;/a&gt; and &lt;a href=&#34;https://github.com/kata-containers/kata-containers/blob/2fc7f75724ac9e18e60f63dcc9aa395dc51c184d/docs/design/architecture.md#networking&#34; target=&#34;_blank&#34;&gt;kata-containers&lt;/a&gt; support multiple network interface with &lt;a href=&#34;https://man7.org/linux/man-pages/man8/tc-mirred.8.html&#34; target=&#34;_blank&#34;&gt;tc redirect&lt;/a&gt;, the same technology that&amp;rsquo;s used by containerlab to run &lt;a href=&#34;https://containerlab.srlinux.dev/manual/vrnetlab/&#34; target=&#34;_blank&#34;&gt;vrnetlab-based images&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, both of these candidates relied on &lt;a href=&#34;https://lwn.net/Articles/556550/&#34; target=&#34;_blank&#34;&gt;virtio VM Sockets&lt;/a&gt; as the communication channel with a VM, which just happened to be one of the features &lt;em&gt;disabled&lt;/em&gt; in Cumulus Linux kernel. So the next option I looked at was Weavework&amp;rsquo;s &lt;a href=&#34;https://github.com/weaveworks/ignite&#34; target=&#34;_blank&#34;&gt;Ignite&lt;/a&gt; and, to my surprise, it worked! I was able to boot the same container image using ignite CLI instead of Docker:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;sudo ignite run --runtime docker --name test --kernel-image networkop/kernel networkop/cx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The kernel image is built from two layers borrowed from an existing Cumulus VX VM &amp;ndash; an uncompressed kernel image and the entire &lt;code&gt;/lib/modules&lt;/code&gt; directory containing loadable kernel modules. The resulting image layer stack looked like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/cumulus-fc.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Finally, I was able to test and confirm that all of the worked-around features that didn&amp;rsquo;t work in a pure container environment worked with ignite. This was a promising first step but there were still a number of key features missing in both containerlab and ignite that needed to be addressed next:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In order to gracefully introduce ignite, containerlab&amp;rsquo;s code had to be refactored to support multiple container runtimes [&lt;a href=&#34;https://github.com/srl-labs/containerlab/pull/416&#34; target=&#34;_blank&#34;&gt;DONE&lt;/a&gt;]&lt;/li&gt;
&lt;li&gt;In order to support custom interface naming, containerlab had to control the assignment of interface MAC addresses [&lt;a href=&#34;https://github.com/srl-labs/containerlab/pull/422&#34; target=&#34;_blank&#34;&gt;DONE&lt;/a&gt;]&lt;/li&gt;
&lt;li&gt;Ignite needed to be extended to support multiple interfaces and stitch them with tc redirect [&lt;a href=&#34;https://github.com/weaveworks/ignite/pull/836&#34; target=&#34;_blank&#34;&gt;PR is merged&lt;/a&gt;]&lt;/li&gt;
&lt;li&gt;A new &lt;code&gt;ignite&lt;/code&gt; runtime needs to be added to containerlab [&lt;a href=&#34;https://containerlab.srlinux.dev/rn/0.15/&#34; target=&#34;_blank&#34;&gt;DONE&lt;/a&gt;]&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One obvious question could be &amp;ndash; is any of this worth the effort? Personally, I had learned so much in the process that my ROI has made it well worth it. For others, I have tried to summarise some of the main reasons why anyone would use containerised Firecracker VMs vs traditional qemu-based VMs in the table below:&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Legacy VMs&lt;/th&gt;
&lt;th&gt;Ignite VMs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UX&lt;/td&gt;
&lt;td&gt;Complex &amp;ndash; Vagrant, Libvirt&lt;/td&gt;
&lt;td&gt;Simple &amp;ndash; containerlab, ignite&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;Legacy, &lt;a href=&#34;https://github.com/qemu/qemu/blob/master/docs/interop/qmp-spec.txt&#34; target=&#34;_blank&#34;&gt;QMP&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Modern, &lt;a href=&#34;https://github.com/firecracker-microvm/firecracker/blob/main/src/api_server/swagger/firecracker.yaml&#34; target=&#34;_blank&#34;&gt;OpenAPI&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Images&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://docs.openstack.org/image-guide/convert-images.html&#34; target=&#34;_blank&#34;&gt;Multiple formats&lt;/a&gt;, mutable&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://github.com/opencontainers/image-spec&#34; target=&#34;_blank&#34;&gt;OCI-standard&lt;/a&gt;, immutable&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Startup configuration&lt;/td&gt;
&lt;td&gt;Ansible, interactive&lt;/td&gt;
&lt;td&gt;Mounting files from host OS&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Distribution&lt;/td&gt;
&lt;td&gt;Individual file sharing&lt;/td&gt;
&lt;td&gt;Container registries&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Startup time&lt;/td&gt;
&lt;td&gt;Tens of seconds&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Scale-out&lt;/td&gt;
&lt;td&gt;Complex and &lt;a href=&#34;https://www.vagrantup.com/docs/multi-machine&#34; target=&#34;_blank&#34;&gt;static&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Standard and &lt;a href=&#34;https://github.com/networkop/k8s-topo&#34; target=&#34;_blank&#34;&gt;dynamic&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;In addition to this, Firecracker&amp;rsquo;s official website provides a list of &lt;a href=&#34;https://firecracker-microvm.github.io/#benefits&#34; target=&#34;_blank&#34;&gt;benefits&lt;/a&gt; and &lt;a href=&#34;https://firecracker-microvm.github.io/#faq&#34; target=&#34;_blank&#34;&gt;FAQ&lt;/a&gt; covering some of the differences with QEMU.&lt;/p&gt;

&lt;h2 id=&#34;plateau-of-productivity&#34;&gt;Plateau of Productivity&lt;/h2&gt;

&lt;p&gt;Although the final stage is still a fair way out, the good news is that I have a stable working prototype that can reliably build Cumulus-based labs so, hopefully, it&amp;rsquo;s only a matter of time before all of the PRs get merged and this functionality becomes available upstream. I also hope this work demonstrates the possibility for other NOSs to ship their virtualised versions as OCI images bundled together with their custom kernels.&lt;/p&gt;

&lt;p&gt;In the meantime, if you&amp;rsquo;re interested, feel free to reach out to me and I&amp;rsquo;ll try to help you get started using containerised Cumulus Linux both on a single node with containerlab and, potentially, even use it for large-scale simulations on top of Kubernetes.&lt;/p&gt;

&lt;h2 id=&#34;july-updates&#34;&gt;July Updates&lt;/h2&gt;

&lt;p&gt;Although it took me a lot longer than I anticipated, I&amp;rsquo;ve managed to merge all of my changes upstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ignite &lt;a href=&#34;https://github.com/weaveworks/ignite/pull/836&#34; target=&#34;_blank&#34;&gt;now supports&lt;/a&gt; connecting arbitrary number of extra interfaces defined in VM&amp;rsquo;s &lt;a href=&#34;https://github.com/weaveworks/ignite/blob/main/pkg/constants/vm.go#L45&#34; target=&#34;_blank&#34;&gt;annotations&lt;/a&gt;. This opens up possibilities beyond the original network simulation use case, allowing Firecracker micro-VMs to transparently interconnect with any interfaces on the host (e.g. via SR-IOV CNI).&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Containerlab release &lt;a href=&#34;https://containerlab.srlinux.dev/rn/0.15/&#34; target=&#34;_blank&#34;&gt;0.15&lt;/a&gt; now includes a special &lt;code&gt;cvx&lt;/code&gt; node that spins up a containerised Cumulus Linux which can be integrated with any number of the &lt;a href=&#34;https://containerlab.srlinux.dev/manual/kinds/kinds/&#34; target=&#34;_blank&#34;&gt;supported nodes&lt;/a&gt; for multi-vendor labs and interop testing. I&amp;rsquo;ve also included a number of labs with different configurations covering everything from the basics of Cumulus Linux operation (&lt;a href=&#34;https://clabs.netdevops.me/rs/cvx03/&#34; target=&#34;_blank&#34;&gt;CTD&lt;/a&gt;) all the way to &lt;a href=&#34;https://clabs.netdevops.me/rs/cvx04/&#34; target=&#34;_blank&#34;&gt;advanced scenarios&lt;/a&gt; like symmetric EVPN with MLAG and MLAG-free multi-homing.&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Getting Started with eBPF and Go</title>
      <link>https://networkop.co.uk/post/2021-03-ebpf-intro/</link>
      <pubDate>Mon, 08 Mar 2021 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2021-03-ebpf-intro/</guid>
      <description>

&lt;p&gt;eBPF has a thriving ecosystem with a plethora of educational resources both on the subject of &lt;a href=&#34;https://ebpf.io/what-is-ebpf/&#34; target=&#34;_blank&#34;&gt;eBPF itself&lt;/a&gt; and its various application, including &lt;a href=&#34;https://github.com/xdp-project/xdp-tutorial&#34; target=&#34;_blank&#34;&gt;XDP&lt;/a&gt;. Where it becomes confusing is when it comes to the choice of libraries and tools to interact with and orchestrate eBPF. Here you have to select between a Python-based &lt;a href=&#34;https://github.com/iovisor/bcc&#34; target=&#34;_blank&#34;&gt;BCC&lt;/a&gt; framework, C-based &lt;a href=&#34;https://github.com/libbpf/libbpf&#34; target=&#34;_blank&#34;&gt;libbpf&lt;/a&gt; and a range of Go-based libraries from &lt;a href=&#34;https://github.com/dropbox/goebpf&#34; target=&#34;_blank&#34;&gt;Dropbox&lt;/a&gt;, &lt;a href=&#34;https://github.com/cilium/ebpf&#34; target=&#34;_blank&#34;&gt;Cilium&lt;/a&gt;, &lt;a href=&#34;https://github.com/aquasecurity/tracee/tree/main/libbpfgo&#34; target=&#34;_blank&#34;&gt;Aqua&lt;/a&gt; and &lt;a href=&#34;https://github.com/projectcalico/felix/tree/master/bpf&#34; target=&#34;_blank&#34;&gt;Calico&lt;/a&gt;. Another important area that is often overlooked is the &amp;ldquo;productionisation&amp;rdquo; of the eBPF code, i.e. going from manually instrumented examples towards production-grade applications like Cilium.
In this post, I&amp;rsquo;ll document some of my findings in this space, specifically in the context of writing a network (XDP) application with a userspace controller written in Go.&lt;/p&gt;

&lt;h2 id=&#34;choosing-an-ebpf-library&#34;&gt;Choosing an eBPF library&lt;/h2&gt;

&lt;p&gt;In most cases, an eBPF library is there to help you achieve two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Load eBPF programs and maps&lt;/strong&gt; into the kernel and perform &lt;a href=&#34;https://kinvolk.io/blog/2018/10/exploring-bpf-elf-loaders-at-the-bpf-hackfest/#common-steps&#34; target=&#34;_blank&#34;&gt;relocations&lt;/a&gt;, associating an eBPF program with the correct map via its file descriptor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interact with eBPF maps&lt;/strong&gt;, allowing all the standard CRUD operations on the key/value pairs stored in those maps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some libraries may also help you attach your eBPF program to a specific &lt;a href=&#34;https://ebpf.io/what-is-ebpf/#hook-overview&#34; target=&#34;_blank&#34;&gt;hook&lt;/a&gt;, although for networking use case this may easily be done with any existing netlink API library.&lt;/p&gt;

&lt;p&gt;When it comes to the choice of an eBPF library, I&amp;rsquo;m not the only one confused (see &lt;a href=&#34;https://twitter.com/maurovasquezb/status/1146438190062063616&#34; target=&#34;_blank&#34;&gt;[1]&lt;/a&gt;,&lt;a href=&#34;https://twitter.com/qeole/status/1364521385138282497&#34; target=&#34;_blank&#34;&gt;[2]&lt;/a&gt;). The truth is each library has its own unique scope and limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;(https://pkg.go.dev/github.com/projectcalico/felix@v3.8.9+incompatible/bpf)&#34; target=&#34;_blank&#34;&gt;Calico&lt;/a&gt; implements a Go wrapper around CLI commands made with &lt;a href=&#34;https://twitter.com/qeole/status/1101450782841466880&#34; target=&#34;_blank&#34;&gt;bpftool&lt;/a&gt; and iproute2.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/aquasecurity/tracee/tree/main/libbpfgo&#34; target=&#34;_blank&#34;&gt;Aqua&lt;/a&gt; implements a Go wrapper around libbpf C library.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/dropbox/goebpf&#34; target=&#34;_blank&#34;&gt;Dropbox&lt;/a&gt; supports a small set of programs but has a very clean and convenient user API.&lt;/li&gt;
&lt;li&gt;IO Visor&amp;rsquo;s &lt;a href=&#34;https://github.com/iovisor/gobpf&#34; target=&#34;_blank&#34;&gt;gobpf&lt;/a&gt; is a collection of go bindings for the BCC framework which has a stronger focus on tracing and profiling.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/cilium/ebpf&#34; target=&#34;_blank&#34;&gt;Cilium and Cloudflare&lt;/a&gt; are maintaining a &lt;a href=&#34;https://linuxplumbersconf.org/event/4/contributions/449/attachments/239/529/A_pure_Go_eBPF_library.pdf&#34; target=&#34;_blank&#34;&gt;pure Go library&lt;/a&gt; (referred to below as &lt;code&gt;libbpf-go&lt;/code&gt;) that abstracts all eBPF syscalls behind a native Go interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For my network-specific use case, I&amp;rsquo;ve ended up using &lt;code&gt;libbpf-go&lt;/code&gt; due to the fact that it&amp;rsquo;s used by Cilium and Cloudflare and has an active community, although I really liked (the simplicity of) the one from Dropbox and could&amp;rsquo;ve used it as well.&lt;/p&gt;

&lt;p&gt;In order to familiarise myself with the development process, I&amp;rsquo;ve decided to implement an XDP cross-connect application, which has a very niche but important &lt;a href=&#34;https://netdevops.me/2021/transparently-redirecting-packets/frames-between-interfaces/&#34; target=&#34;_blank&#34;&gt;use case&lt;/a&gt; in network topology emulation. The goal is to have an application that watches a configuration file and ensures that local interfaces are interconnected according to the YAML spec from that file. Here is a high-level overview of how &lt;a href=&#34;https://github.com/networkop/xdp-xconnect&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;xdp-xconnect&lt;/code&gt;&lt;/a&gt; works:&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/xdp-xconnect.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;The following sections will describe the application build and delivery process step-by-step, focusing more on integration and less on the actual code. Full code for &lt;code&gt;xdp-xconnect&lt;/code&gt; is &lt;a href=&#34;https://github.com/networkop/xdp-xconnect&#34; target=&#34;_blank&#34;&gt;available&lt;/a&gt; on Github.&lt;/p&gt;

&lt;h2 id=&#34;step-1-writing-the-ebpf-code&#34;&gt;Step 1 - Writing the eBPF code&lt;/h2&gt;

&lt;p&gt;Normally this would be the main section of any &amp;ldquo;Getting Started with eBPF&amp;rdquo; article, however this time it&amp;rsquo;s not the focus. I don&amp;rsquo;t think I can help others learn how to write eBPF, however, I can refer to some very good resources that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generic eBPF theory is covered in a lot of details on &lt;a href=&#34;https://ebpf.io/what-is-ebpf/&#34; target=&#34;_blank&#34;&gt;ebpf.io&lt;/a&gt; and Cilium&amp;rsquo;s eBPF and XDP &lt;a href=&#34;https://docs.cilium.io/en/stable/bpf/&#34; target=&#34;_blank&#34;&gt;reference guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The best place for some hands-on practice with eBPF and XDP is the &lt;a href=&#34;https://github.com/xdp-project/xdp-tutorial&#34; target=&#34;_blank&#34;&gt;xdp-tutorial&lt;/a&gt;. It&amp;rsquo;s an amazing resource that is definitely worth reading even if you don&amp;rsquo;t end up doing the assignments.&lt;/li&gt;
&lt;li&gt;Cilium &lt;a href=&#34;https://github.com/cilium/cilium/tree/master/bpf&#34; target=&#34;_blank&#34;&gt;source code&lt;/a&gt; and it&amp;rsquo;s analysis in &lt;a href=&#34;https://k8s.networkop.co.uk/cni/cilium/#a-day-in-the-life-of-a-packet&#34; target=&#34;_blank&#34;&gt;[1]&lt;/a&gt; and &lt;a href=&#34;http://arthurchiao.art/blog/cilium-life-of-a-packet-pod-to-service/&#34; target=&#34;_blank&#34;&gt;[2]&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My eBPF program is very simple, it consists of a single call to an eBPF &lt;a href=&#34;https://man7.org/linux/man-pages/man7/bpf-helpers.7.html&#34; target=&#34;_blank&#34;&gt;helper function &lt;/a&gt;, which redirects &lt;em&gt;all&lt;/em&gt; packets from one interface to another based on the index of the incoming interface.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-c&#34;&gt;#include &amp;lt;linux/bpf.h&amp;gt;
#include &amp;lt;bpf/bpf_helpers.h&amp;gt;

SEC(&amp;quot;xdp&amp;quot;)
int  xdp_xconnect(struct xdp_md *ctx)
{
    return bpf_redirect_map(&amp;amp;xconnect_map, ctx-&amp;gt;ingress_ifindex, 0);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In order to compile the above program, we need to provide search paths for all the included header files. The easiest way to do that is to make a copy of everything under &lt;a href=&#34;https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/tree/tools/lib/bpf&#34; target=&#34;_blank&#34;&gt;linux/tools/lib/bpf/&lt;/a&gt;, however, this will include a lot of unnecessary files. So an alternative is to create a list of dependencies:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ clang -MD -MF xconnect.d -target bpf -I ~/linux/tools/lib/bpf -c xconnect.c
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we can make a local copy of only a small number of files specified in &lt;code&gt;xconnect.d&lt;/code&gt; and use the following command to compile eBPF code for the local CPU architecture:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ clang -target bpf -Wall -O2 -emit-llvm -g -Iinclude -c xconnect.c -o - | \
llc -march=bpf -mcpu=probe -filetype=obj -o xconnect.o
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The resulting ELF file is what we&amp;rsquo;d need to provide to our Go library in the next step.&lt;/p&gt;

&lt;h2 id=&#34;step-2-writing-the-go-code&#34;&gt;Step 2 - Writing the Go code&lt;/h2&gt;

&lt;p&gt;Compiled eBPF programs and maps can be loaded by &lt;code&gt;libbpf-go&lt;/code&gt; with just a few instructions. By adding a struct with &lt;code&gt;ebpf&lt;/code&gt; tags we can automate the relocation procedure so that our program knows where to find its map.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;spec, err := ebpf.LoadCollectionSpec(&amp;quot;ebpf/xconnect.o&amp;quot;)
if err != nil {
  panic(err)
}

var objs struct {
	XCProg  *ebpf.Program `ebpf:&amp;quot;xdp_xconnect&amp;quot;`
	XCMap   *ebpf.Map     `ebpf:&amp;quot;xconnect_map&amp;quot;`
}
if err := spec.LoadAndAssign(&amp;amp;objs, nil); err != nil {
	panic(err)
}
defer objs.XCProg.Close()
defer objs.XCMap.Close()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Type &lt;code&gt;ebpf.Map&lt;/code&gt; has a set of methods that perform standard CRUD operations on the contents of the loaded map:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;err = objs.XCMap.Put(uint32(0), uint32(10))

var v0 uint32
err = objs.XCMap.Lookup(uint32(0), &amp;amp;v0)

err = objs.XCMap.Delete(uint32(0))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The only step that&amp;rsquo;s not covered by &lt;code&gt;libbpf-go&lt;/code&gt; is the attachment of programs to network hooks. This, however, can easily be accomplished by any existing netlink library, e.g. &lt;a href=&#34;https://github.com/vishvananda/netlink&#34; target=&#34;_blank&#34;&gt;vishvananda/netlink&lt;/a&gt;, by associating a network link with a file descriptor of the loaded program:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;link, err := netlink.LinkByName(&amp;quot;eth0&amp;quot;)
err = netlink.LinkSetXdpFdWithFlags(*link, c.objs.XCProg.FD(), 2)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note that I&amp;rsquo;m using the &lt;a href=&#34;https://github.com/torvalds/linux/blob/master/tools/include/uapi/linux/if_link.h#L966&#34; target=&#34;_blank&#34;&gt;SKB_MODE&lt;/a&gt; XDP flag to work around the exiting veth driver &lt;a href=&#34;https://github.com/xdp-project/xdp-tutorial/tree/master/packet03-redirecting#sending-packets-back-to-the-interface-they-came-from&#34; target=&#34;_blank&#34;&gt;caveat&lt;/a&gt;. Although the native XDP mode is &lt;a href=&#34;https://www.netronome.com/media/images/fig3.width-800.png&#34; target=&#34;_blank&#34;&gt;considerably faster&lt;/a&gt; than any other eBPF hook, SKB_MODE may not be as fast due to the fact that packet headers have to be pre-parsed by the network stack (see &lt;a href=&#34;https://www.youtube.com/watch?v=q3gjNe6LKDI&#34; target=&#34;_blank&#34;&gt;video&lt;/a&gt;).&lt;/p&gt;

&lt;h2 id=&#34;step-3-code-distribution&#34;&gt;Step 3 - Code Distribution&lt;/h2&gt;

&lt;p&gt;At this point everything should have been ready to package and ship our application if it wasn&amp;rsquo;t for one problem - eBPF &lt;a href=&#34;https://facebookmicrosites.github.io/bpf/blog/2020/02/19/bpf-portability-and-co-re.html#the-problem-of-bpf-portability&#34; target=&#34;_blank&#34;&gt;code portability&lt;/a&gt;. Historically, this process involved copying of the eBPF source code to the target platform, pulling in the required kernel headers and compiling it for the specific kernel version. This problem is especially pronounced for tracing/monitoring/profiling use cases which may require access to pretty much any kernel data structure, so the only solution is to introduce another layer of indirection (see &lt;a href=&#34;https://facebookmicrosites.github.io/bpf/blog/2020/02/19/bpf-portability-and-co-re.html&#34; target=&#34;_blank&#34;&gt;CO-RE&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Network use cases, on the other hand, rely on a relatively small and stable subset of kernel types, so they don&amp;rsquo;t suffer from the same kind of problems as their tracing and profiling counterparts. Based on what I&amp;rsquo;ve seen so far, the two most common code packaging approaches are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ship eBPF code together with the required kernel headers, assuming they match the underlying kernel (see &lt;a href=&#34;https://github.com/cilium/cilium/tree/master/bpf&#34; target=&#34;_blank&#34;&gt;Cilium&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Ship eBPF code and pull in the kernel headers on the target platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In both of these cases, the eBPF code is still compiled on that target platform which is an extra step that needs to be performed before the user-space application can start. However, there&amp;rsquo;s an alternative, which is to pre-compile the eBPF code and only ship the ELF files. This is exactly what can be done with &lt;a href=&#34;https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;bpf2go&lt;/code&gt;&lt;/a&gt;, which can embed the compiled code into a Go package. It relies on &lt;code&gt;go generate&lt;/code&gt; to produce a &lt;a href=&#34;https://github.com/networkop/xdp-xconnect/blob/main/pkg/xdp/xdp_bpf.go&#34; target=&#34;_blank&#34;&gt;new file&lt;/a&gt; with compiled eBPF and &lt;code&gt;libbpf-go&lt;/code&gt; skeleton code, the only requirement being the &lt;a href=&#34;https://github.com/networkop/xdp-xconnect/blob/main/pkg/xdp/xdp.go#L14&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;//go:generate&lt;/code&gt;&lt;/a&gt; instruction. Once generated though, our eBPF program can be loaded with just a few lines (note the absence of any arguments):&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;specs, err := newXdpSpecs()
objs, err := specs.Load(nil)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The obvious benefit of this approach is that we no longer need to compile on the target machine and can ship both eBPF and userspace Go code in a single package or Go binary. This is great because it allows us to use our application not only as a binary but also import it into any 3rd party Go applications (see &lt;a href=&#34;https://github.com/networkop/xdp-xconnect#usage&#34; target=&#34;_blank&#34;&gt;usage example&lt;/a&gt;).&lt;/p&gt;

&lt;h2 id=&#34;reading-and-interesting-references&#34;&gt;Reading and Interesting References&lt;/h2&gt;

&lt;p&gt;Generic Theory:&lt;br /&gt;
&lt;a href=&#34;https://github.com/xdp-project/xdp-tutorial&#34; target=&#34;_blank&#34;&gt;https://github.com/xdp-project/xdp-tutorial&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://docs.cilium.io/en/stable/bpf/&#34; target=&#34;_blank&#34;&gt;https://docs.cilium.io/en/stable/bpf/&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/&#34; target=&#34;_blank&#34;&gt;https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;BCC and libbpf:&lt;br /&gt;
&lt;a href=&#34;https://facebookmicrosites.github.io/bpf/blog/2020/02/20/bcc-to-libbpf-howto-guide.html&#34; target=&#34;_blank&#34;&gt;https://facebookmicrosites.github.io/bpf/blog/2020/02/20/bcc-to-libbpf-howto-guide.html&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://nakryiko.com/posts/libbpf-bootstrap/&#34; target=&#34;_blank&#34;&gt;https://nakryiko.com/posts/libbpf-bootstrap/&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://pingcap.com/blog/why-we-switched-from-bcc-to-libbpf-for-linux-bpf-performance-analysis&#34; target=&#34;_blank&#34;&gt;https://pingcap.com/blog/why-we-switched-from-bcc-to-libbpf-for-linux-bpf-performance-analysis&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://facebookmicrosites.github.io/bpf/blog/&#34; target=&#34;_blank&#34;&gt;https://facebookmicrosites.github.io/bpf/blog/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;eBPF/XDP performance:&lt;br /&gt;
&lt;a href=&#34;https://www.netronome.com/blog/bpf-ebpf-xdp-and-bpfilter-what-are-these-things-and-what-do-they-mean-enterprise/&#34; target=&#34;_blank&#34;&gt;https://www.netronome.com/blog/bpf-ebpf-xdp-and-bpfilter-what-are-these-things-and-what-do-they-mean-enterprise/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Linus Kernel Coding Style:&lt;br /&gt;
&lt;a href=&#34;https://www.kernel.org/doc/html/v5.9/process/coding-style.html&#34; target=&#34;_blank&#34;&gt;https://www.kernel.org/doc/html/v5.9/process/coding-style.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;libbpf-go&lt;/code&gt; example programs:&lt;br /&gt;
&lt;a href=&#34;https://github.com/takehaya/goxdp-template&#34; target=&#34;_blank&#34;&gt;https://github.com/takehaya/goxdp-template&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://github.com/hrntknr/nfNat&#34; target=&#34;_blank&#34;&gt;https://github.com/hrntknr/nfNat&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://github.com/takehaya/Vinbero&#34; target=&#34;_blank&#34;&gt;https://github.com/takehaya/Vinbero&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://github.com/tcfw/vpc&#34; target=&#34;_blank&#34;&gt;https://github.com/tcfw/vpc&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://github.com/florianl/tc-skeleton&#34; target=&#34;_blank&#34;&gt;https://github.com/florianl/tc-skeleton&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://github.com/cloudflare/rakelimit&#34; target=&#34;_blank&#34;&gt;https://github.com/cloudflare/rakelimit&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://github.com/b3a-dev/ebpf-geoip-demo&#34; target=&#34;_blank&#34;&gt;https://github.com/b3a-dev/ebpf-geoip-demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bpf2go&lt;/code&gt;:&lt;br /&gt;
&lt;a href=&#34;https://github.com/lmb/ship-bpf-with-go&#34; target=&#34;_blank&#34;&gt;https://github.com/lmb/ship-bpf-with-go&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go&#34; target=&#34;_blank&#34;&gt;https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;XDP example programs:&lt;br /&gt;
&lt;a href=&#34;https://github.com/cpmarvin/lnetd-ctl&#34; target=&#34;_blank&#34;&gt;https://github.com/cpmarvin/lnetd-ctl&lt;/a&gt;&lt;br /&gt;
&lt;a href=&#34;https://gitlab.com/mwiget/crpd-l2tpv3-xdp&#34; target=&#34;_blank&#34;&gt;https://gitlab.com/mwiget/crpd-l2tpv3-xdp&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Building your own SD-WAN with Envoy and Wireguard</title>
      <link>https://networkop.co.uk/post/2021-02-diy-sdwan/</link>
      <pubDate>Sat, 13 Feb 2021 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2021-02-diy-sdwan/</guid>
      <description>

&lt;p&gt;When using a personal VPN at home, one of the biggest problems I&amp;rsquo;ve faced was the inability to access public streaming services. I don&amp;rsquo;t care about watching Netflix from another country, I just want to be able to use my local internet connection for this kind of traffic while still encrypting everything else. This problem is commonly known in network engineering as &amp;ldquo;local internet breakout&amp;rdquo; and is often implemented at remote branch/edge sites to save costs of transporting SaaS traffic (e.g. Office365) over the VPN infrastructure. These &amp;ldquo;local breakout&amp;rdquo; solutions often rely on &lt;a href=&#34;https://sdwan-docs.cisco.com/Product_Documentation/Software_Features/SD-WAN_Release_16.2/07Policy_Applications/04Using_a_vEdge_Router_as_a_NAT_Device/Configuring_Local_Internet_Exit&#34; target=&#34;_blank&#34;&gt;explicit enumeration&lt;/a&gt; of all public IP subnets, which is a bit &lt;a href=&#34;https://docs.microsoft.com/en-gb/microsoft-365/enterprise/urls-and-ip-address-ranges?view=o365-worldwide&#34; target=&#34;_blank&#34;&gt;cumbersome&lt;/a&gt;, or require &amp;ldquo;intelligent&amp;rdquo; (i.e. expensive) &lt;a href=&#34;https://www.silver-peak.com/products/unity-edge-connect/first-packet-iq&#34; target=&#34;_blank&#34;&gt;DPI&lt;/a&gt; functionality. However, it is absolutely possible to build something like this for personal use and this post will demonstrate how to do that.&lt;/p&gt;

&lt;h2 id=&#34;solution-overview&#34;&gt;Solution Overview&lt;/h2&gt;

&lt;p&gt;The problem scope consists of two relatively independent areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Traffic routing&lt;/strong&gt; - how to forward traffic to different outgoing interfaces based on the target domain.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;VPN management&lt;/strong&gt; - how to connect to the best VPN gateway and make sure that connection stays healthy.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of one these problem areas is addressed by a separate set of components.&lt;/p&gt;

&lt;p&gt;VPN management is solved by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;smart-vpn-client&lt;/strong&gt; agent that discovers all of the available VPN gateways, connects to the closest one and continuously monitors the state of that connection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traffic routing is solved by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A transparent proxy (&lt;strong&gt;Envoy&lt;/strong&gt;), capable of domain- and SNI-based routing and binding to multiple outgoing interfaces.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;A proxy controller called &lt;strong&gt;envoy-split-proxy&lt;/strong&gt;, that monitors the user intent (what traffic to route where) and ensures that Envoy configuration is updated accordingly.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An extra bonus is a free-tier monitoring solution based on &lt;a href=&#34;https://grafana.com/products/cloud/pricing/&#34; target=&#34;_blank&#34;&gt;Grafana Cloud&lt;/a&gt; that scrapes local metrics and pushes them to the managed observability platform.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/sd-wan.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Below, I&amp;rsquo;ll walk through the component design and steps of how to deploy this solution on a Linux-based ARM64 box (in my case it&amp;rsquo;s a Synology NAS). The only two prerequisites that are not covered in this blogpost are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker support on the target ARM64 box (see &lt;a href=&#34;https://github.com/markdumay/synology-docker&#34; target=&#34;_blank&#34;&gt;this guide&lt;/a&gt; for Synology)&lt;/li&gt;
&lt;li&gt;Wireguard kernel module loaded on the target ARM64 box (see &lt;a href=&#34;https://github.com/runfalk/synology-wireguard&#34; target=&#34;_blank&#34;&gt;this guide&lt;/a&gt;  for Synology)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;smart-vpn-client&#34;&gt;Smart VPN Client&lt;/h2&gt;

&lt;p&gt;At its core, the &lt;a href=&#34;https://github.com/networkop/smart-vpn-client&#34; target=&#34;_blank&#34;&gt;smart-vpn-client&lt;/a&gt; implements a standard set of functions you&amp;rsquo;d expect from a VPN client, i.e.:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discovers all of the available VPN gateways (exit nodes) it can connect to.&lt;/li&gt;
&lt;li&gt;Measures the latency and selects the &amp;ldquo;closest&amp;rdquo; gateway for higher &lt;a href=&#34;https://en.wikipedia.org/wiki/Bandwidth-delay_product&#34; target=&#34;_blank&#34;&gt;throughput&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Configures the wireguard interface and associated &lt;a href=&#34;https://www.wireguard.com/netns/#routing-all-your-traffic&#34; target=&#34;_blank&#34;&gt;routing policies&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only supported VPN provider at this stage is PIA, so the discovery and VPN setup is based on the instructions from the &lt;a href=&#34;https://github.com/pia-foss/manual-connections&#34; target=&#34;_blank&#34;&gt;pia-foss repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &amp;ldquo;smart&amp;rdquo; functionality is designed to maintain a consistent user experience in the presence of network congestion and VPN gateway overloading and it does that by resetting a VPN connection if it becomes too slow or unresponsive. Translated to technical terms, this is implemented as the following sequence of steps :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a new VPN connection is set up, we record the &amp;ldquo;baseline&amp;rdquo; round-trip time over it.&lt;/li&gt;
&lt;li&gt;Connection health monitor periodically measures the RTT and maintains a record of the last 10 values.&lt;/li&gt;
&lt;li&gt;At the end of each measurement, connection health is evaluated and can be deemed degraded if either:

&lt;ul&gt;
&lt;li&gt;No response was received within a timeout window of 10s.&lt;/li&gt;
&lt;li&gt;The exponentially weighted average of the last 10 measurements exceeded 10x the &amp;ldquo;baseline&amp;rdquo;.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;If health stays degraded for 3 consecutive measurement intervals, the VPN connection is re-established to the new &amp;ldquo;closest&amp;rdquo; VPN gateway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The VPN client binary can be built from &lt;a href=&#34;https://github.com/networkop/smart-vpn-client&#34; target=&#34;_blank&#34;&gt;source&lt;/a&gt; or downloaded as a docker image, which is how I&amp;rsquo;m deploying it:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
docker pull networkop/smart-vpn-client

docker rm -f vpn
docker run --privileged networkop/smart-vpn-client -cleanup
docker run -d --name vpn --restart always --net host \
--env VPN_PWD=&amp;lt;VPN-PASSWORD&amp;gt; \
--privileged \
networkop/smart-vpn-client \
-user &amp;lt;VPN-USER&amp;gt; -ignore=uk_2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The above script creates a new container attached to the root network namespace. We can see the main steps it went through in the logs:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ docker logs vpn
level=info msg=&amp;quot;Starting VPN Connector&amp;quot;
level=info msg=&amp;quot;Ignored headends: [uk_2]&amp;quot;
level=info msg=&amp;quot;VPN provider is PIA&amp;quot;
level=info msg=&amp;quot;Discovering VPN headends for PIA&amp;quot;
level=info msg=&amp;quot;Winner is uk with latency 14 ms&amp;quot;
level=info msg=&amp;quot;Brining up WG tunnel to 143.X.X.X:1337&amp;quot;
level=info msg=&amp;quot;Wireguard Tunnel is UP&amp;quot;
level=info msg=&amp;quot;New baseline is 202 ms; Threshold is 2020&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we can verify that the wireguard tunnel has been set up:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ sudo wg show
interface: wg-pia
  public key: MY_PUBLIC_KEY
  private key: (hidden)
  listening port: 34006
  fwmark: 0xea55

peer: PEER_PUBLIC_KEY
  endpoint: 143.X.X.X:1337
  allowed ips: 0.0.0.0/0
  latest handshake: 1 minute, 21 seconds ago
  transfer: 3.29 GiB received, 1.03 GiB sent
  persistent keepalive: every 15 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;envoy-split-proxy&#34;&gt;Envoy Split Proxy&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Split_tunneling&#34; target=&#34;_blank&#34;&gt;Split tunneling&lt;/a&gt; is a technique commonly used in VPN access to enable local internet breakout for some subset of user traffic. It works at Layer 3, so the decision is made based on the contents of a local routing table. What I&amp;rsquo;ve done with Envoy is effectively taken the same idea and extended it to L4-L7, hence the name &lt;strong&gt;split proxy&lt;/strong&gt;. The goal was to make L4-L7 split-routing completely transparent to the end-user, with no extra requirements (e.g. no custom proxy configuration) apart from a default route pointing at the ARM64 box. This goal is achieved by a combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Envoy proxy acting as a configurable dataplane for L4-L7 traffic.&lt;/li&gt;
&lt;li&gt;IPTables redirecting all inbound TCP/80 and TCP/443 traffic to envoy listeners.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol&#34; target=&#34;_blank&#34;&gt;XDS&lt;/a&gt; controller that configures envoy to act as a transparent forward proxy based on the user intent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user intent is expressed as a YAML file with the list of domains and the non-default interface to bind to when making outgoing requests. This file is watched by the envoy-split-proxy application and applied to envoy on every detected change.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;interface: eth0
urls:
## Netflix
- netflix.com
- &amp;quot;*.nflxso.net&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;All other domains will be proxied and sent out the default (wireguard) interface, so the above file only defines the exceptions. One obvious problem is that streaming services will most likely use a combination of domains, not just their well-known second-level domains. The domain &lt;a href=&#34;https://github.com/networkop/envoy-split-proxy#discovering-domain-names&#34; target=&#34;_blank&#34;&gt;discovery process&lt;/a&gt; may be a bit tedious but only needs to be done once for a single streaming service. Some of the domains that I use are already &lt;a href=&#34;https://github.com/networkop/envoy-split-proxy/blob/main/split.yaml&#34; target=&#34;_blank&#34;&gt;documented&lt;/a&gt; in the source code repository.&lt;/p&gt;

&lt;p&gt;Similar to the VPN client, all software can be deployed directly on ARM64 box as binaries, or as docker containers. Regardless of the deployment method, the two prerequisites are the user intent YAML file and the Envoy bootstrap configuration that makes it connect to the XDS controller.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ curl -O https://raw.githubusercontent.com/networkop/envoy-split-proxy/main/envoy.yaml
$ curl -O https://raw.githubusercontent.com/networkop/envoy-split-proxy/main/split.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With those files in the &lt;code&gt;pwd&lt;/code&gt; we can spin up the two docker containers with the following script:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh

docker pull networkop/envoy-split-proxy
docker pull envoyproxy/envoy:v1.16.2

docker rm -f app
docker rm -f envoy

docker run -d --name app --restart always --net host \
-v $(pwd)/split.yaml:/split.yaml \
networkop/envoy-split-proxy \
-conf /split.yaml

docker run -d --name envoy --restart always --net host \
-v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml \
envoyproxy/envoy:v1.16.2 \
--config-path /etc/envoy/envoy.yaml \
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, all transit traffic needs to get redirected to envoy with a couple of iptable rules:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
sudo iptables -t nat -D PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 10000
sudo iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 10001

sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 10000
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 10001
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;monitoring&#34;&gt;Monitoring&lt;/h2&gt;

&lt;p&gt;Observability is the critical part of any &amp;ldquo;software-defined&amp;rdquo; networking product, so our solution shouldn&amp;rsquo;t be an exception. It&amp;rsquo;s &lt;a href=&#34;https://nleiva.medium.com/monitoring-your-home-lab-devices-in-the-cloud-for-free-54c4d11ac471&#34; target=&#34;_blank&#34;&gt;even easier&lt;/a&gt; when we don&amp;rsquo;t have to manage it ourselves. Thanks to Grafana Cloud&amp;rsquo;s &lt;a href=&#34;https://grafana.com/blog/2021/01/12/the-new-grafana-cloud-the-only-composable-observability-stack-for-metrics-logs-and-traces-now-with-free-and-paid-plans-to-suit-every-use-case/&#34; target=&#34;_blank&#34;&gt;forever free plan&lt;/a&gt;, all we have to do is deploy a grafana agent and scrape metrics exposed by envoy and smart-vpn-client. In order to save on resource utilisation (both local and cloud), I&amp;rsquo;ve disabled some of the less interesting collectors and dropped most of the envoy metrics, so that the final configuration file looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;integrations:
  node_exporter:
    enabled: true
    disable_collectors:
      - bonding
      - infiniband
      - ipvs
      - mdadm
      - nfs
      - nfsd
      - xfs
      - zfs
      - arp
      - btrfs
      - bcache
      - edac
      - entropy
      - pressure
      - rapl
      - softnet
  prometheus_remote_write:
    - basic_auth:
        password: &amp;lt;PWD&amp;gt;
        username: &amp;lt;USERNAME&amp;gt;
      url: https://prometheus.grafana.net/api/prom/push
prometheus:
  configs:
    - name: integrations
      remote_write:
        - basic_auth:
            password: &amp;lt;PWD&amp;gt;
            username: &amp;lt;USERNAME&amp;gt;
          url: https://prometheus.grafana.net/api/prom/push
      scrape_configs:
      - job_name: vpn
        scrape_interval: 5s
        static_configs:
        - targets: [&#39;localhost:2112&#39;]
      - job_name: envoy
        metrics_path: /stats/prometheus
        metric_relabel_configs:
        - source_labels: [__name__]
          regex: &amp;quot;.+_ms_bucket&amp;quot;
          action: keep
        - source_labels: [envoy_cluster_name]
          regex: &amp;quot;xds_cluster&amp;quot;
          action: drop
        static_configs:
        - targets: [&#39;localhost:19000&#39;]
  global:
    scrape_interval: 15s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The script to enable grafana agent simply mounts the above configuration file and points the agent at it:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh

docker rm -f agent
docker run -d --name agent --restart always --net host \
-v /tmp/grafana-agent-wal:/etc/agent \
-v $(pwd)/config.yaml:/etc/agent-config/agent.yaml \
grafana/agent:v0.12.0 --config.file=/etc/agent-config/agent.yaml --prometheus.wal-directory=/etc/agent/data
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The collected metrics can be displayed in a beautiful dashboard allowing us to correlate network throughput, VPN healthchecks and proxy connection latencies.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/sdwan-dashboard.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;credits&#34;&gt;Credits&lt;/h2&gt;

&lt;p&gt;Building something like this would have been a lot more difficult without other FOSS projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/envoyproxy/envoy&#34; target=&#34;_blank&#34;&gt;Envoy&lt;/a&gt; proxy - the most versatile and feature-rich proxy in the world today.&lt;/li&gt;
&lt;li&gt;Wireguard and &lt;a href=&#34;https://github.com/WireGuard/wgctrl-go&#34; target=&#34;_blank&#34;&gt;wgctrl&lt;/a&gt; Go package to manage all interface-related configurations.&lt;/li&gt;
&lt;li&gt;Grafana Cloud&amp;rsquo;s with their &lt;a href=&#34;https://grafana.com/products/cloud/pricing/&#34; target=&#34;_blank&#34;&gt;free tier plan&lt;/a&gt; which is a perfect fit for personal/home use.&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Self-hosted external DNS resolver for Kubernetes</title>
      <link>https://networkop.co.uk/post/2020-08-k8s-gateway/</link>
      <pubDate>Fri, 14 Aug 2020 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2020-08-k8s-gateway/</guid>
      <description>

&lt;p&gt;There comes a time in the life of every Kubernetes cluster when internal resources (pods, deployments) need to be exposed to the outside world. Doing so from a pure IP connectivity perspective is relatively easy as most of the constructs come baked-in (e.g. NodePort-type Services) or can be enabled with an off-the-shelf add-on (e.g. Ingress and LoadBalancer controllers). In this post, we&amp;rsquo;ll focus on one crucial piece of network connectivity which glues together the dynamically-allocated external IP with a static customer-defined hostname — a DNS. We&amp;rsquo;ll examine the pros and cons of various ways of implementing external DNS in Kubernetes and introduce a new CoreDNS plugin that can be used for dynamic discovery and resolution of multiple types of external Kubernetes resources.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/d11.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;external-kubernetes-resources&#34;&gt;External Kubernetes Resources&lt;/h2&gt;

&lt;p&gt;Let&amp;rsquo;s start by reviewing various types of &amp;ldquo;external&amp;rdquo; Kubernetes resources and the level of networking abstraction they provide starting from the lowest all the way to the highest level.&lt;/p&gt;

&lt;p&gt;One of the most fundamental building block of all things external in Kubernetes is the &lt;strong&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/service/#nodeport&#34; target=&#34;_blank&#34;&gt;NodePort&lt;/a&gt;&lt;/strong&gt; service. It works by allocating a unique external port for every service instance and setting up kube-proxy to deliver incoming packets from that port to the one of the healthy backend pods. This service is rarely used on its own and was designed to be a building block for other higher-level resources.&lt;/p&gt;

&lt;p&gt;Next level up is the &lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;LoadBalancer&lt;/strong&gt;&lt;/a&gt; service which is one of the most common ways of exposing services externally. This service type requires an extra controller that will be responsible for IP address allocation and delivering traffic to the Kubernetes nodes. This function can be implemented by cloud load-balancers, in case the cluster is deployed one of the public clouds, a physical appliance or a cluster add-on like &lt;a href=&#34;https://github.com/metallb/metallb&#34; target=&#34;_blank&#34;&gt;MetalLB&lt;/a&gt; or &lt;a href=&#34;https://github.com/kubesphere/porter&#34; target=&#34;_blank&#34;&gt;Porter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At the highest level of abstraction is the &lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/ingress/&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;Ingress&lt;/strong&gt;&lt;/a&gt; resource. It, too, requires a dedicated controller which spins up and configures a number of proxy servers that can act as a L7 load-balancer, API gateway or, in some cases, a L4 (TCP/UDP) proxy. Similarly to the LoadBalancer, Ingress may be implemented by one of the public cloud L7 load-balancers or could be self-hosted by the cluster using any one of the &lt;a href=&#34;https://docs.google.com/spreadsheets/d/16bxRgpO1H_Bn-5xVZ1WrR_I-0A-GOI6egmhvqqLMOmg/edit#gid=1612037324&#34; target=&#34;_blank&#34;&gt;open-source ingress controllers&lt;/a&gt;. Amongst other things, Ingress controllers can perform &lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/ingress/#tls&#34; target=&#34;_blank&#34;&gt;TLS offloading&lt;/a&gt; and &lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/ingress/#name-based-virtual-hostinghttps://kubernetes.io/docs/concepts/services-networking/ingress/#name-based-virtual-hosting&#34; target=&#34;_blank&#34;&gt;named-based routing&lt;/a&gt; which rely heavily on external DNS infrastructure that can dynamically discover Ingress resources as they get added/removed from the cluster.&lt;/p&gt;

&lt;p&gt;There are other external-ish resources like &lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/service/&#34; target=&#34;_blank&#34;&gt;ExternalName&lt;/a&gt; services and even ClusterIP in &lt;a href=&#34;https://docs.projectcalico.org/networking/advertise-service-ips&#34; target=&#34;_blank&#34;&gt;certain cases&lt;/a&gt;. They represent a very small subset of corner case scenarios and are considered outside of the scope of this article. Instead, we&amp;rsquo;ll focus on the two most widely used external resources—LoadBalancers and Ingresses, and see how they can be integrated into the public DNS infrastructure.&lt;/p&gt;

&lt;h2 id=&#34;externaldns&#34;&gt;ExternalDNS&lt;/h2&gt;

&lt;p&gt;The most popular solution today is the &lt;a href=&#34;https://github.com/kubernetes-sigs/external-dns&#34; target=&#34;_blank&#34;&gt;ExternalDNS controller&lt;/a&gt;. It works by integrating with one of the public DNS providers and populates a pre-configured DNS zone with entries extracted from the monitored objects, e.g. Ingress&amp;rsquo;s &lt;code&gt;spec.rules[*].host&lt;/code&gt; or Service&amp;rsquo;s &lt;code&gt;external-dns.alpha.kubernetes.io/hostname&lt;/code&gt; annotations. In addition, it natively supports non-standard resources like Istio&amp;rsquo;s Gateway or Contour&amp;rsquo;s IngressRoute which, together with the support for over 15 cloud DNS providers, makes it a default choice for anyone approaching this problem for the first time.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/d12.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;ExternalDNS is an ideal solution for Kubernetes clusters under a single administrative domain, however, it does have a number of trade-offs that start to manifest themselves when a cluster is shared among multiple tenants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple DNZ zones require a dedicated ExternalDNS instance per zone.&lt;/li&gt;
&lt;li&gt;Each new zone requires cloud-specific IAM rules to be set up to allow ExternalDNS to make the required changes.&lt;/li&gt;
&lt;li&gt;Unless managing a local cloud DNS, API credentials will need to be stored as a secret inside the cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addition to the above, ExternalDNS represents another layer of abstraction and complexity outside of the cluster that needs to be considered during maintenance and troubleshooting. Every time the controller fails, there&amp;rsquo;s a possibility of some stale state to be left, accumulating over time and polluting the hosted DNS zone.&lt;/p&gt;

&lt;h2 id=&#34;coredns-s-k8s-external-plugin&#34;&gt;CoreDNS&amp;rsquo;s &lt;code&gt;k8s_external&lt;/code&gt; plugin&lt;/h2&gt;

&lt;p&gt;An alternative approach is to make internal Kubernetes DNS add-on respond to external DNS queries. The prime example of this is the CoreDNS &lt;a href=&#34;https://coredns.io/plugins/k8s_external/&#34; target=&#34;_blank&#34;&gt;k8s_external&lt;/a&gt; plugin. It works by configuring CoreDNS to respond to external queries matching a number of pre-configured domains. For example, the following configuration will allow it to resolve queries for &lt;code&gt;svc2.ns.mydomain.com&lt;/code&gt;, as shown in the diagram above, as well as the &lt;code&gt;svc2.ns.example.com&lt;/code&gt; domain:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;k8s_external mydomain.com example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Both queries will return the same set of IP addresses extracted from the &lt;code&gt;.status.loadBalancer&lt;/code&gt; field of the &lt;code&gt;svc2&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;These domains will still need to be delegated, which means you will need to expose CoreDNS externally with service type LoadBalancer and update NS records with the provisioned IP address.&lt;/p&gt;

&lt;p&gt;Under the hood, &lt;code&gt;k8s_external&lt;/code&gt; relies on the main &lt;a href=&#34;https://coredns.io/plugins/kubernetes/&#34; target=&#34;_blank&#34;&gt;kubernetes&lt;/a&gt; plugin and simply re-uses information already collected by it. This presents a problem when trying to add extra resources (e.g. Ingresses, Gateways) as these changes will increase the amount of information the main plugin needs to process and will inevitably affect its performance. This is why there&amp;rsquo;s a new plugin now that&amp;rsquo;s designed to absorb and extend the functionality of the &lt;code&gt;k8s_external&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&#34;the-new-k8s-gateway-coredns-plugin&#34;&gt;The new &lt;code&gt;k8s_gateway&lt;/code&gt; CoreDNS plugin&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/ori-edge/k8s_gateway&#34; target=&#34;_blank&#34;&gt;This out-of-tree plugin&lt;/a&gt; is loosely based on the &lt;code&gt;k8s_external&lt;/code&gt; and maintains a similar configuration syntax, however it does contain a few notable differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It doesn&amp;rsquo;t rely on any other plugin and uses its own mechanism of Kubernetes object discovery.&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s designed to be used alongside (and not replace) an existing internal DNS plugin, be it kube-dns or CoreDNS.&lt;/li&gt;
&lt;li&gt;It doesn&amp;rsquo;t collect or expose any internal cluster IP addresses.&lt;/li&gt;
&lt;li&gt;It supports both LoadBalancer services and Ingresses with an eye on the service API&amp;rsquo;s &lt;a href=&#34;https://github.com/kubernetes-sigs/service-apis/blob/master/examples/basic-http.yaml#L29&#34; target=&#34;_blank&#34;&gt;HTTPRoute&lt;/a&gt; when it becomes available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/d13.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;The way it&amp;rsquo;s designed to be used can be summarised as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The scope of the plugin is controlled by a set of RBAC rules and by default is limited to List/Watch operations on Ingress and Service resources.&lt;/li&gt;
&lt;li&gt;The plugin is &lt;a href=&#34;https://github.com/ori-edge/k8s_gateway#build&#34; target=&#34;_blank&#34;&gt;built&lt;/a&gt; as a CoreDNS binary and run as a deployment.&lt;/li&gt;
&lt;li&gt;This deployment is exposed externally and the required domains are delegated to the address of the external load-balancer.&lt;/li&gt;
&lt;li&gt;Any DNS query that reaches the &lt;code&gt;k8s_gateway&lt;/code&gt; plugin will go through the following stages:

&lt;ul&gt;
&lt;li&gt;First, it will be matched against one of the zones configured for this plugin in the Corefile.&lt;/li&gt;
&lt;li&gt;If there&amp;rsquo;s a hit, the next step is to match it against any of the existing Ingress resources. The lookup is performed against FQDNs configured in &lt;code&gt;spec.rules[*].host&lt;/code&gt; fields of the Ingress.&lt;/li&gt;
&lt;li&gt;At this stage, the result can be returned to the user with IPs collected from the &lt;code&gt;.status.loadBalancer.ingress&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If no matching Ingress was found, the search continues with the Services objects. Since services don&amp;rsquo;t really have domain names, the lookup is performed using the &lt;code&gt;serviceName.namespace&lt;/code&gt; as the key.&lt;/li&gt;
&lt;li&gt;If there&amp;rsquo;s a match, it is returned to the end-user in a similar way, alternatively the plugin responds with &lt;code&gt;NXDOMAIN&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design of the &lt;code&gt;k8s_gateway&lt;/code&gt; plugin attempts to address some of the issues of other solutions described above, but also brings a number of extra advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All external DNS entries and associated state are contained within the Kubernetes cluster while the hosted zone only contains a single NS record.&lt;/li&gt;
&lt;li&gt;You get the power and flexibility of the full suite of CoreDNS&amp;rsquo;s &lt;a href=&#34;https://coredns.io/plugins/&#34; target=&#34;_blank&#34;&gt;internal&lt;/a&gt; and &lt;a href=&#34;https://coredns.io/explugins/&#34; target=&#34;_blank&#34;&gt;external&lt;/a&gt; plugins, e.g. you can use ACL to control which source IPs are (not)allowed to make queries.&lt;/li&gt;
&lt;li&gt;Provisioning that doesn&amp;rsquo;t rely on annotations makes it easier to maintain Kubernetes manifests.&lt;/li&gt;
&lt;li&gt;Separate deployment means that internal DNS resolution is not affected in case external DNS becomes overloaded.&lt;/li&gt;
&lt;li&gt;Since API keys are &lt;strong&gt;not&lt;/strong&gt; stored in the cluster, it makes it easier and safer for new tenants to bring their own domain.&lt;/li&gt;
&lt;li&gt;Federated Kubernetes cluster deployments (e.g. using &lt;a href=&#34;https://github.com/kubernetes-sigs/cluster-api&#34; target=&#34;_blank&#34;&gt;Cluster API&lt;/a&gt;) become easier as there&amp;rsquo;s only a single entrypoint via the management cluster and each workload cluster can get its own self-hosted subdomain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/d14.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;k8s_gateway&lt;/code&gt; is developed as an out-of-tree plugin under an open-source license. Community contributions in the form of issues, pull requests and documentation are always &lt;a href=&#34;https://github.com/ori-edge/k8s_gateway&#34; target=&#34;_blank&#34;&gt;welcomed&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Anatomy of the &#34;kubernetes.default&#34;</title>
      <link>https://networkop.co.uk/post/2020-06-kubernetes-default/</link>
      <pubDate>Mon, 29 Jun 2020 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2020-06-kubernetes-default/</guid>
      <description>

&lt;p&gt;Every Kubernetes cluster is provisioned with a special service that provides a way for internal applications to talk to the API server. However, unlike the rest of the components that get spun up by default, you won&amp;rsquo;t find the definition of this service in any of the static manifests and this is just one of the many things that make this service unique.&lt;/p&gt;

&lt;h2 id=&#34;the-special-one&#34;&gt;The Special One&lt;/h2&gt;

&lt;p&gt;To make sure we&amp;rsquo;re on the same page, I&amp;rsquo;m talking about this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubect get svc kubernetes -n default
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   161m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This service is unique in many ways. First, as you may have noticed, it always occupies the first available IP in the Cluster CIDR, a.k.a. &lt;code&gt;--service-cluster-ip-range&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Second, this service is invincible, i.e. it will always get re-created, even when it&amp;rsquo;s manually removed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   118s
$ kubectl delete svc kubernetes
service &amp;quot;kubernetes&amp;quot; deleted
$ kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   0s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You may notice that it comes up with the same ClusterIP, regardless of how many services may already exist in the cluster.&lt;/p&gt;

&lt;p&gt;Third, this service does not have any matching pods, however it does have a fully populated &lt;code&gt;Endpoints&lt;/code&gt; object:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get pod --selector component=apiserver --all-namespaces
No resources found
$ kubectl get endpoints kubernetes
NAME         ENDPOINTS                                         AGE
kubernetes   172.18.0.2:6443,172.18.0.3:6443,172.18.0.4:6443   4m16s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This last bit is perhaps the most curious one. How can a service have a list of endpoints when there are no pods that match this service&amp;rsquo;s label selector? This goes against how services controller &lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service&#34; target=&#34;_blank&#34;&gt;works&lt;/a&gt;.  Note that this behaviour is true even for managed kubernetes clusters, where the API server is run by the provider (e.g. GKE).&lt;/p&gt;

&lt;p&gt;Finally, the IP and Port of this service get injected into every pod as environment variables:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;These values can later be used by k8s controllers to &lt;a href=&#34;https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/client_config.go#L561&#34; target=&#34;_blank&#34;&gt;configure&lt;/a&gt; the client-go&amp;rsquo;s rest interface that is used to establish connectivity to the API server:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;func InClusterConfig() (*Config, error) {

  host := os.Getenv(&amp;quot;KUBERNETES_SERVICE_HOST&amp;quot;), 
  port := os.Getenv(&amp;quot;KUBERNETES_SERVICE_PORT&amp;quot;)

  return &amp;amp;Config{
		Host: &amp;quot;https://&amp;quot; + net.JoinHostPort(host, port),
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;controller-of-controllers&#34;&gt;Controller of controllers&lt;/h2&gt;

&lt;p&gt;To find out who&amp;rsquo;s behind this magical service, we need to look at the code for the k/k&amp;rsquo;s &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/master/pkg/master/controller.go&#34; target=&#34;_blank&#34;&gt;master controller&lt;/a&gt;, that is described as the &amp;ldquo;controller manager for the core bootstrap Kubernetes controller loops&amp;rdquo;, meaning it&amp;rsquo;s one of the first controllers that gets spun up by the API server binary. Let&amp;rsquo;s break it down into smaller pieces and see what&amp;rsquo;s going on inside it.&lt;/p&gt;

&lt;p&gt;When the controller is started, it spins up a runner, which is a group of functions that run forever until they receive a stop signal via a channel.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// Start begins the core controller loops that must exist for bootstrapping
// a cluster.
func (c *Controller) Start() {
  
	c.runner = async.NewRunner(c.RunKubernetesNamespaces, c.RunKubernetesService, repairClusterIPs.RunUntil, repairNodePorts.RunUntil)
	c.runner.Start()
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The most interesting is the second function - &lt;code&gt;RunKubernetesService()&lt;/code&gt;, which is a control loop that constantly updates the default kubernetes service.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// RunKubernetesService periodically updates the kubernetes service
func (c *Controller) RunKubernetesService(ch chan struct{}) {

	if err := c.UpdateKubernetesService(false); err != nil {
		runtime.HandleError(fmt.Errorf(&amp;quot;unable to sync kubernetes service: %v&amp;quot;, err))
	}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Most of the work is done by the &lt;code&gt;UpdateKubernetesService()&lt;/code&gt;. This function does three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates the &amp;ldquo;default&amp;rdquo; namespace whose name is defined in the &lt;code&gt;metav1.NamespaceDefault&lt;/code&gt; variable.&lt;/li&gt;
&lt;li&gt;Creates/Updates the default kuberentes service.&lt;/li&gt;
&lt;li&gt;Creates/Updates the endpoints resource for this service.&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// UpdateKubernetesService attempts to update the default Kube service.
func (c *Controller) UpdateKubernetesService(reconcile bool) error {

	if err := createNamespaceIfNeeded(c.NamespaceClient, metav1.NamespaceDefault); err != nil {
		return err
   }

	if err := c.CreateOrUpdateMasterServiceIfNeeded(kubernetesServiceName, c.ServiceIP, servicePorts, serviceType, reconcile); err != nil {
		return err
	}

	if err := c.EndpointReconciler.ReconcileEndpoints(kubernetesServiceName, c.PublicIP, endpointPorts, reconcile); err != nil {
		return err
	}

	return nil
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, the &lt;code&gt;CreateOrUpdateMasterServiceIfNeeded()&lt;/code&gt; function is where the default service is being built. You can see the skeleton of this service&amp;rsquo;s object in the below snippet:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;const kubernetesServiceName = &amp;quot;kubernetes&amp;quot;

// CreateOrUpdateMasterServiceIfNeeded will create the specified service if it
// doesn&#39;t already exist.
func (c *Controller) CreateOrUpdateMasterServiceIfNeeded(serviceName string, serviceIP net.IP, servicePorts []corev1.ServicePort, serviceType corev1.ServiceType, reconcile bool) error {

	svc := &amp;amp;corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      serviceName,
			Namespace: metav1.NamespaceDefault,
			Labels:    map[string]string{&amp;quot;provider&amp;quot;: &amp;quot;kubernetes&amp;quot;, &amp;quot;component&amp;quot;: &amp;quot;apiserver&amp;quot;},
		},
		Spec: corev1.ServiceSpec{
			Ports: servicePorts,
			// maintained by this code, not by the pod selector
			Selector:        nil,
			ClusterIP:       serviceIP.String(),
			SessionAffinity: corev1.ServiceAffinityNone,
			Type:            serviceType,
		},
	}

	_, err := c.ServiceClient.Services(metav1.NamespaceDefault).Create(context.TODO(), svc, metav1.CreateOptions{})

	return err
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The code above explains why this service can never be completely removed from the cluster - the master controller loop will always recreate it if it&amp;rsquo;s missing, along with its endpoints object. However, this still doesn&amp;rsquo;t explain how the IP for this service is selected nor where the endpoint IPs are coming from. In order to do this, we need to get a deeper look at how the API server builds its runtime configuration.&lt;/p&gt;

&lt;h2 id=&#34;always-the-first&#34;&gt;Always the first&lt;/h2&gt;

&lt;p&gt;One of the interesting qualities of the ClusterIP of the &lt;code&gt;kubernetes.default&lt;/code&gt; is that it always (unless manually overridden) occupies the first IP in the Cluster CIDR. The answer is hidden in the &lt;code&gt;ServiceIPRange()&lt;/code&gt; function of the master controller&amp;rsquo;s &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/master/pkg/master/services.go&#34; target=&#34;_blank&#34;&gt;service.go&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;
func ServiceIPRange(passedServiceClusterIPRange net.IPNet) (net.IPNet, net.IP, error) {

	size := integer.Int64Min(utilnet.RangeSize(&amp;amp;serviceClusterIPRange), 1&amp;lt;&amp;lt;16)
	if size &amp;lt; 8 {
		return net.IPNet{}, net.IP{}, fmt.Errorf(&amp;quot;the service cluster IP range must be at least %d IP addresses&amp;quot;, 8)
	}

	// Select the first valid IP from ServiceClusterIPRange to use as the GenericAPIServer service IP.
	apiServerServiceIP, err := utilnet.GetIndexedIP(&amp;amp;serviceClusterIPRange, 1)
	if err != nil {
		return net.IPNet{}, net.IP{}, err
	}

	return serviceClusterIPRange, apiServerServiceIP, nil
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This function gets &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/master/pkg/master/master.go#L292&#34; target=&#34;_blank&#34;&gt;called&lt;/a&gt; when the master controller is started and hard-codes the service IP for the default service to the first IP of the range. Another interesting fact in this function is that it always checks that the Cluster IP range is at least /29, which fits 6 usable addresses in the worst case. The latter can probably be explained by the fact that the next size down is /30, which doesn&amp;rsquo;t leave much room for user-defined clusterIPs after the &lt;code&gt;kubernetes.default&lt;/code&gt; and &lt;code&gt;kube-dns.kube-system&lt;/code&gt; are configured, so in the smallest possible cluster you can at least configure a few non-default services before you run out of IPs.&lt;/p&gt;

&lt;h2 id=&#34;endpoint-ips&#34;&gt;Endpoint IPs&lt;/h2&gt;

&lt;p&gt;The way endpoint addresses are populated is different between managed (GKE, AKS, EKS) and non-managed clusters. Let&amp;rsquo;s first have a look at a highly-available kind cluster:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl describe svc kubernetes | grep Endpoints
Endpoints:         172.18.0.3:6443,172.18.0.4:6443,172.18.0.7:6443
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Bearing in mind that by default kind would use &lt;code&gt;10.244.0.0/16&lt;/code&gt; as the pod IP range and &lt;code&gt;10.96.0.0/12&lt;/code&gt; as the cluster IP range, these IPs don&amp;rsquo;t make a lot of sense. However, since kind uses kubeadm under the hood, which spins up control plane components as static pods, we can find API server pods in the &lt;code&gt;kube-system&lt;/code&gt; namespace:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl -n kube-system get pod -l tier=control-plane -o wide | grep api
kube-apiserver-kind-control-plane             1/1     Running   172.18.0.3
kube-apiserver-kind-control-plane2            1/1     Running   172.18.0.4
kube-apiserver-kind-control-plane3            1/1     Running   172.18.0.7
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If we check the manifest of any of the above pods, we&amp;rsquo;ll see that they are run with &lt;code&gt;hostNetwork: true&lt;/code&gt; and those IP come from the underlying containers that kind uses as nodes. As a part of the &lt;code&gt;UpdateKubernetesService()&lt;/code&gt; mentioned above, each API server in the cluster goes and &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/master/pkg/master/controller.go#L243&#34; target=&#34;_blank&#34;&gt;updates&lt;/a&gt; the &lt;code&gt;endpoints&lt;/code&gt; object with its own IP and Port as defined in the &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/master/pkg/master/reconcilers/mastercount.go#L62&#34; target=&#34;_blank&#34;&gt;mastercount.go&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;func (r *masterCountEndpointReconciler) ReconcileEndpoints(serviceName string, ip net.IP, endpointPorts []corev1.EndpointPort, reconcilePorts bool) error {

	e.Subsets = []corev1.EndpointSubset{{
		Addresses: []corev1.EndpointAddress{{IP: ip.String()}},
		Ports:     endpointPorts,
	}}
	klog.Warningf(&amp;quot;Resetting endpoints for master service %q to %#v&amp;quot;, serviceName, e)
	_, err = r.epAdapter.Update(metav1.NamespaceDefault, e)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;p&gt;With managed Kubernetes clusters, control-plane nodes are not accessible by end users, so it&amp;rsquo;s harder to say exactly how endpoints are getting populated. However, it&amp;rsquo;s fairly easy to imagine that a cloud provider spins up a 3-node control-plane with a load-balancer and configures all three API servers with this LB&amp;rsquo;s IP as the &lt;code&gt;advertise-address&lt;/code&gt;. This would results in a single endpoint that represents that managed control-plane load-balancer:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get ep kubernetes
NAME         ENDPOINTS          AGE
kubernetes   172.16.0.2:443   40d
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Solving the Ingress Mystery Puzzle</title>
      <link>https://networkop.co.uk/post/2020-06-ingress-puzzle/</link>
      <pubDate>Sat, 13 Jun 2020 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2020-06-ingress-puzzle/</guid>
      <description>

&lt;p&gt;Last week I posted a &lt;a href=&#34;https://twitter.com/networkop1/status/1269651463690760193&#34; target=&#34;_blank&#34;&gt;tweet&lt;/a&gt; about a Kubernetes networking puzzle. In this post, we&amp;rsquo;ll go over the details of this puzzle and uncover the true cause and motive of the misbehaving ingress.&lt;/p&gt;

&lt;h2 id=&#34;puzzle-recap&#34;&gt;Puzzle recap&lt;/h2&gt;

&lt;p&gt;Imagine you have a Kubernetes cluster with three namespaces, each with its own namespace-scoped ingress controller. You&amp;rsquo;ve created an ingress in each namespace that exposes a simple web application. You&amp;rsquo;ve checked one of them, made sure it works and moved on to other things. However some time later, you get reports that the web app is unavailable. You go to check it again and indeed, the page is not responding, although nothing has changed in the cluster. In fact, you realise that the problem is intermittent - one minute you can access the page, and on the next refresh it&amp;rsquo;s gone. To make things worse, you realise that similar issues affect the other two ingresses.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/ingress-puzzle.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;If you feel like you&amp;rsquo;re capable of solving it on your own, feel free to follow the steps in the &lt;a href=&#34;https://github.com/networkop/ingress-puzzle&#34; target=&#34;_blank&#34;&gt;walkthrough&lt;/a&gt;, otherwise, continue on reading. In either case, make sure you&amp;rsquo;ve setup a local test environment so that it&amp;rsquo;s easier to follow along:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Clone the ingress-puzzle repo:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git clone https://github.com/networkop/ingress-puzzle &amp;amp;&amp;amp; cd ingress-puzzle
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Build a local test cluster:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make cluster
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Create three namespaces:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make namespaces
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Create an in-cluster load-balancer (MetalLB) that will allocate IPs from a &lt;code&gt;100.64.0.0/16&lt;/code&gt; range:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make load-balancer
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;In each namespace, install a namespace-scoped ingress controller:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make controllers
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Create three test deployments and expose them via ingresses:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make ingresses
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;ingress-controller-expected-behaviour&#34;&gt;Ingress controller expected behaviour&lt;/h2&gt;

&lt;p&gt;In order to solve this puzzle we need to understand how ingress controllers perform their duties, so let&amp;rsquo;s see how a typical ingress controller does that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An ingress controller consists of &lt;strong&gt;two components&lt;/strong&gt; - control plane and data plane, which can be run separately or be a part of the same pod/deployment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Control plane&lt;/strong&gt; is a k8s controller that uses its pod&amp;rsquo;s service account to talk to the API server and establishes &amp;ldquo;watches&amp;rdquo; on &lt;code&gt;Ingress&lt;/code&gt;-type resources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data plane&lt;/strong&gt; is a reverse proxy (e.g. nginx, envoy) that receives traffic from end users and forwards it upstream to one of the backend k8s services.&lt;/li&gt;
&lt;li&gt;In order to steer the traffic to the data plane, an external &lt;strong&gt;load-balancer&lt;/strong&gt; service is required, whose address (IP or hostname) is reflected in ingress&amp;rsquo;s status field.&lt;/li&gt;
&lt;li&gt;As &lt;code&gt;Ingress&lt;/code&gt; resources get created/deleted, controller updates configuration of its data plane to match the desired state described in those resources.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This sounds simple enough, but as always, the devil is in the details, so let&amp;rsquo;s start by focusing on one of the namespaces and observe the behaviour of its ingress.&lt;/p&gt;

&lt;h2 id=&#34;exhibit-1-namespace-one&#34;&gt;Exhibit #1 - namespace one&lt;/h2&gt;

&lt;p&gt;Let&amp;rsquo;s looks at the ingress in namespace &lt;code&gt;one&lt;/code&gt;. It seems like a healthy-looking output, the address is set to the &lt;code&gt;100.64.0.0&lt;/code&gt; which is part of the MetalLB range.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubens one
$ kubectl get ingress
NAME   CLASS    HOSTS   ADDRESS      PORTS   AGE
test   &amp;lt;none&amp;gt;   *       100.64.0.0   80      141m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you want to test connectivity to the backend deployment, you can add the MetalLB public IP range to the docker bridge like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ip=$(kubectl get nodes -o jsonpath=&#39;{.items[0].status.addresses[0].address}&#39;)
device=$(ip -j route get $ip | jq &#39;.[0].dev&#39;)
sudo ip addr add 100.64.0.100/16 dev $device
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now you should be able to hit the test nginx deployment:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl -s 100.64.0.0 | grep Welcome
&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
&amp;lt;h1&amp;gt;Welcome to nginx!&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nothing unusual so far, and nothing to indicate intermittent connectivity either. Let&amp;rsquo;s move on.&lt;/p&gt;

&lt;h2 id=&#34;exhibit-2-namespaces-two-three&#34;&gt;Exhibit #2 - namespaces two &amp;amp; three&lt;/h2&gt;

&lt;p&gt;This output looks a bit weird, the IP in the address field is definitely not a part of the MetalLB range:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubens two
$ kubectl get ingress
NAME   CLASS    HOSTS   ADDRESS      PORTS   AGE
test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      155m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A similar situation can be observed in the other namespace:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubens three
$ kubectl get ingress
NAME   CLASS    HOSTS   ADDRESS      PORTS   AGE
test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      155m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, these outputs don&amp;rsquo;t make a lot of sense. How can two different ingresses, controlled by two distinct controllers have the same address? And why do they get allocated with a private IP, which is not managed by MetalLB? If we check services across all existing namespaces, there won&amp;rsquo;t be a single service with IPs from &lt;code&gt;172.16.0.0/12&lt;/code&gt; range.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl get svc -A | grep 172
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;exhibit-4-flapping-addresses&#34;&gt;Exhibit #4 - flapping addresses&lt;/h2&gt;

&lt;p&gt;Another one of the reported issues was the intermittent connectivity to some of the ingresses. If we keep watching the ingress in namespace &lt;code&gt;one&lt;/code&gt;, we should see something interesting:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubens one
kubectl get ingress --watch
NAME   CLASS    HOSTS   ADDRESS      PORTS   AGE
test   &amp;lt;none&amp;gt;   *       100.64.0.0   80      141m
test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      141m
test   &amp;lt;none&amp;gt;   *       100.64.0.0   80      142m
test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      142m
test   &amp;lt;none&amp;gt;   *       100.64.0.0   80      143m
test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      143m
test   &amp;lt;none&amp;gt;   *       100.64.0.0   80      144m
test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      144m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It looks like the ingress address is flapping between our &amp;ldquo;good&amp;rdquo; MetalLB IP and the same exact IP that the other two ingresses have. Now let&amp;rsquo;s zoom out a bit and have a look at all three ingresses at the same time:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl get ingress --watch -A
NAMESPACE   NAME   CLASS    HOSTS   ADDRESS      PORTS   AGE
one         test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      150m
three       test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      150m
two         test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      150m
one         test   &amp;lt;none&amp;gt;   *       100.64.0.0   80      150m
three       test   &amp;lt;none&amp;gt;   *       100.64.0.2   80      151m
three       test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      151m
one         test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      151m
one         test   &amp;lt;none&amp;gt;   *       100.64.0.0   80      151m
three       test   &amp;lt;none&amp;gt;   *       100.64.0.2   80      152m
one         test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      152m
three       test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      152m
one         test   &amp;lt;none&amp;gt;   *       100.64.0.0   80      152m

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This looks even more puzzling - it seems that all ingresses have addresses that flap continuously. This would definitely explain the intermittent connectivity, however the most important question now is &amp;ldquo;why&amp;rdquo;.&lt;/p&gt;

&lt;h2 id=&#34;exhibit-5-controller-logs&#34;&gt;Exhibit #5 - controller logs&lt;/h2&gt;

&lt;p&gt;The most obvious suspect at this stage is the ingress controller, since it&amp;rsquo;s the one that updates the status of its managed ingress resources. Let stay in the same namespace and look at its logs:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl logs deploy/ingress-ingress-nginx-controller -f

event.go:278] Event(v1.ObjectReference{Kind:&amp;quot;Ingress&amp;quot;, Namespace:&amp;quot;one&amp;quot;, Name:&amp;quot;test&amp;quot;, UID:&amp;quot;7d1e4069-d285-4cf8-ba28-437d0a8fd04d&amp;quot;, APIVersion:&amp;quot;networking.k8s.io/v1beta1&amp;quot;, ResourceVersion:&amp;quot;55860&amp;quot;, FieldPath:&amp;quot;&amp;quot;}): type: &#39;Normal&#39; reason: &#39;UPDATE&#39; Ingress one/test

status.go:275] updating Ingress one/test status from [{172.18.0.2 }] to [{100.64.0.0 }]

event.go:278] Event(v1.ObjectReference{Kind:&amp;quot;Ingress&amp;quot;, Namespace:&amp;quot;one&amp;quot;, Name:&amp;quot;test&amp;quot;, UID:&amp;quot;7d1e4069-d285-4cf8-ba28-437d0a8fd04d&amp;quot;, APIVersion:&amp;quot;networking.k8s.io/v1beta1&amp;quot;, ResourceVersion:&amp;quot;55870&amp;quot;, FieldPath:&amp;quot;&amp;quot;}): type: &#39;Normal&#39; reason: &#39;UPDATE&#39; Ingress one/test
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This doesn&amp;rsquo;t make a lot of sense - the ingress controller clearly updates the status with the right IP, but why does it get overwritten? and by whom?&lt;/p&gt;

&lt;h2 id=&#34;exhibit-5-cluster-wide-logs&#34;&gt;Exhibit #5 - cluster-wide logs&lt;/h2&gt;

&lt;p&gt;At this point, we can allow ourselves a little bit of cheating. Since it&amp;rsquo;s a test cluster and we&amp;rsquo;ve only got a few ingresses configured, we can tail logs from all ingress controllers and watch all ingresses at the same time. Don&amp;rsquo;t forget to install &lt;a href=&#34;https://github.com/wercker/stern&#34; target=&#34;_blank&#34;&gt;stern&lt;/a&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl get ingress -A -w &amp;amp;
stern --all-namespaces -l app.kubernetes.io/name=ingress-nginx &amp;amp;
three ingress-ingress-nginx-controller-58b79c576b-94v8d controller status.go:275] updating Ingress three/test status from [{172.18.0.2 }] to [{100.64.0.2 }]

three       test   &amp;lt;none&amp;gt;   *       100.64.0.2   80      174m

twothree  ingress-ingress-nginx-controller-5db5984d7d-vljth ingress-ingress-nginx-controller-58b79c576b-94v8d controller event.go:278] Event(v1.ObjectReference{Kind:&amp;quot;Ingress&amp;quot;, Namespace:&amp;quot;three&amp;quot;, Name:&amp;quot;test&amp;quot;, UID:&amp;quot;176f0f8e-d3d5-4476-9b51-2d02c7eb47e2&amp;quot;, APIVersion:&amp;quot;networking.k8s.io/v1beta1&amp;quot;, ResourceVersion:&amp;quot;57195&amp;quot;, FieldPath:&amp;quot;&amp;quot;}): type: &#39;Normal&#39; reason: &#39;UPDATE&#39; Ingress three/test
event.go:278] Event(v1.ObjectReference{Kind:&amp;quot;Ingress&amp;quot;, Namespace:&amp;quot;three&amp;quot;, Name:&amp;quot;test&amp;quot;, UID:&amp;quot;176f0f8e-d3d5-4476-9b51-2d02c7eb47e2&amp;quot;, APIVersion:&amp;quot;networking.k8s.io/v1beta1&amp;quot;, ResourceVersion:&amp;quot;57195&amp;quot;, FieldPath:&amp;quot;&amp;quot;}): type: &#39;Normal&#39; reason: &#39;UPDATE&#39; Ingress three/test

two ingress-ingress-nginx-controller-5db5984d7d-vljth controller status.go:275] updating Ingress one/test status from [{100.64.0.0 }] to [{172.18.0.2 }]
two ingress-ingress-nginx-controller-5db5984d7d-vljth controller status.go:275] updating Ingress three/test status from [{100.64.0.2 }] to [{172.18.0.2 }]

three       test   &amp;lt;none&amp;gt;   *       172.18.0.2   80      174m
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;whodunit&#34;&gt;Whodunit&lt;/h2&gt;

&lt;p&gt;So, it looks like the culprit is the ingress controller in namespace &lt;code&gt;two&lt;/code&gt; and it tries to change status fields of all three ingresses. Now it&amp;rsquo;s safe to look at exactly how it was installed, and this is the helm values file:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;controller:
  publishService:
    enabled: false
    pathOverride: &amp;quot;two/svc&amp;quot;
  scope:
    enabled: false
  admissionWebhooks:
    enabled: false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It looks like the scope variable is set incorrectly so the ingress controller defaults to trying to manage ingresses across all namespaces. This should be an easy fix - just change the scope to &lt;code&gt;true&lt;/code&gt; and upgrade the chart.&lt;/p&gt;

&lt;p&gt;However, this still doesn&amp;rsquo;t explain the private IP address or its origin. Let&amp;rsquo;s try the following command:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl get nodes -o wide
NAME                           STATUS   ROLES    AGE    VERSION   INTERNAL-IP
ingress-puzzle-control-plane   Ready    master   5h3m   v1.18.2   172.18.0.2 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So this is where it comes from - it&amp;rsquo;s the IP of the k8s node we&amp;rsquo;ve been running our tests on. But why would it get allocated to an ingress? To understand that we need to have a look at
 the nginx-ingress controller source code, specifically this function from &lt;a href=&#34;https://github.com/kubernetes/ingress-nginx/blob/master/internal/ingress/status/status.go#L174&#34; target=&#34;_blank&#34;&gt;status.go&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;func (s *statusSync) runningAddresses() ([]string, error) {
	if s.PublishStatusAddress != &amp;quot;&amp;quot; {
		return []string{s.PublishStatusAddress}, nil
	}

	if s.PublishService != &amp;quot;&amp;quot; {
		return statusAddressFromService(s.PublishService, s.Client)
	}

	// get information about all the pods running the ingress controller
	pods, err := s.Client.CoreV1().Pods(s.pod.Namespace).List(context.TODO(), metav1.ListOptions{
		LabelSelector: labels.SelectorFromSet(s.pod.Labels).String(),
	})
	if err != nil {
		return nil, err
	}

	addrs := make([]string, 0)
	for _, pod := range pods.Items {
		// only Running pods are valid
		if pod.Status.Phase != apiv1.PodRunning {
			continue
		}

		name := k8s.GetNodeIPOrName(s.Client, pod.Spec.NodeName, s.UseNodeInternalIP)
		if !sliceutils.StringInSlice(name, addrs) {
			addrs = append(addrs, name)
		}
	}

	return addrs, nil
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is how the nginx-ingress controller determines the address to report in the ingress status:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check if the address is statically set with the &lt;code&gt;--publish-status-address&lt;/code&gt; flag.&lt;/li&gt;
&lt;li&gt;Try to collect addresses from a published service (load-balancer).&lt;/li&gt;
&lt;li&gt;If both of the above have failed, get the list of pods and return the IPs of the nodes they are running on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This last bit is why we had that private IP in the status field. If you look at the above values YAML again, you&amp;rsquo;ll see that the &lt;code&gt;publishService&lt;/code&gt; value is overridden with a static service called &lt;code&gt;svc&lt;/code&gt;. However, because this service doesn&amp;rsquo;t exist and was never created, the ingress controller will fail to collect the right IP and will fall through to step 3.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/ingress-puzzle-solved.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;The logic described above is quite common and is also implemented by &lt;a href=&#34;https://github.com/Kong/kubernetes-ingress-controller/blob/master/internal/ingress/status/status.go&#34; target=&#34;_blank&#34;&gt;Kong&lt;/a&gt; ingress controller. The idea is that if your k8s nodes are running in a cluster with public IPs, this should still make the ingress accessible, even without a load-balancer.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Getting Started with Cluster API using Docker</title>
      <link>https://networkop.co.uk/post/2020-05-cluster-api-intro/</link>
      <pubDate>Sun, 03 May 2020 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2020-05-cluster-api-intro/</guid>
      <description>

&lt;p&gt;Cluster API (CAPI) is a relatively new project aimed at deploying Kubernetes clusters using a declarative API (think YAML). The official documentation (a.k.a. the Cluster API book), does a very good job explaining the main &lt;a href=&#34;https://cluster-api.sigs.k8s.io/user/concepts.html&#34; target=&#34;_blank&#34;&gt;concepts&lt;/a&gt; and &lt;a href=&#34;https://cluster-api.sigs.k8s.io/introduction.html&#34; target=&#34;_blank&#34;&gt;goals&lt;/a&gt; of the project. I always find that one of the best ways to explore new technology is to see how it works locally, on my laptop, and Cluster API has a special &amp;ldquo;Docker&amp;rdquo; infrastructure provider (CAPD) specifically for that. However, the official documentation for how to setup a docker managed cluster is very poor and fractured. In this post, I&amp;rsquo;ll try to demonstrate the complete journey to deploy a single CAPI-managed k8s cluster and provide some explanation of what happens behind the scene so that its easier to troubleshoot when things go wrong.&lt;/p&gt;

&lt;h2 id=&#34;prerequisites&#34;&gt;Prerequisites&lt;/h2&gt;

&lt;p&gt;Two things must be pre-installed before we can start building our test clusters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://kind.sigs.k8s.io/docs/user/quick-start/&#34; target=&#34;_blank&#34;&gt;kind&lt;/a&gt;&lt;/strong&gt; - a tool to setup k8s clusters in docker containers, it will be used as a management (a.k.a. bootstrap) cluster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://cluster-api.sigs.k8s.io/user/quick-start.html#install-clusterctl&#34; target=&#34;_blank&#34;&gt;clusterctl&lt;/a&gt;&lt;/strong&gt; - a command line tool to interact with the management cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We&amp;rsquo;re gonna need run a few scripts from the Cluster API Github repo, so let&amp;rsquo;s get a copy of it locally:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git clone --depth=1 git@github.com:kubernetes-sigs/cluster-api.git &amp;amp;&amp;amp; cd cluster-api
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When building a management cluster with kind, it&amp;rsquo;s a good idea to mount the &lt;code&gt;docker.sock&lt;/code&gt; file from your host OS into the kind cluster, as it is mentioned in &lt;a href=&#34;https://cluster-api.sigs.k8s.io/clusterctl/developers.html#additional-steps-in-order-to-use-the-docker-provider&#34; target=&#34;_blank&#34;&gt;the book&lt;/a&gt;. This will allow you to see the CAPD-managed nodes directly in your hostOS as regular docker containers.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cat &amp;gt; kind-cluster-with-extramounts.yaml &amp;lt;&amp;lt;EOF
kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
nodes:
  - role: control-plane
    extraMounts:
      - hostPath: /var/run/docker.sock
        containerPath: /var/run/docker.sock
EOF
kind create cluster --config ./kind-cluster-with-extramounts.yaml --name clusterapi
kubectl cluster-info --context kind-clusterapi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this stage you should have your kubectl pointed at the new kind cluster, which can be verified like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl get nodes -o wide
NAME                       STATUS   ROLES    AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE       KERNEL-VERSION          CONTAINER-RUNTIME
clusterapi-control-plane   Ready    master   66s   v1.17.0   172.17.0.2    &amp;lt;none&amp;gt;        Ubuntu 19.10   5.6.6-200.fc31.x86_64   containerd://1.3.2
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;preparing-a-capd-controller&#34;&gt;Preparing a CAPD controller&lt;/h2&gt;

&lt;p&gt;The docker image for the CAPD controller is not available in the public registry, so we need to build it locally. The following two commands will build the image and update the installation manifests to use that image.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make -C test/infrastructure/docker docker-build REGISTRY=gcr.io/k8s-staging-capi-docker
make -C test/infrastructure/docker generate-manifests REGISTRY=gcr.io/k8s-staging-capi-docker
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next, we need side-load this image into a kind cluster to make it available to the future CAPD deployment&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kind load docker-image --name clusterapi gcr.io/k8s-staging-capi-docker/capd-manager-amd64:dev
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;setting-up-a-docker-provider&#34;&gt;Setting up a Docker provider&lt;/h2&gt;

&lt;p&gt;Once again, following &lt;a href=&#34;https://cluster-api.sigs.k8s.io/clusterctl/developers.html#additional-steps-in-order-to-use-the-docker-provider&#34; target=&#34;_blank&#34;&gt;the book&lt;/a&gt;, we need to run a local override script to generate a set of manifests for Docker provider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cat &amp;gt; clusterctl-settings.json &amp;lt;&amp;lt;EOF
{
  &amp;quot;providers&amp;quot;: [&amp;quot;cluster-api&amp;quot;,&amp;quot;bootstrap-kubeadm&amp;quot;,&amp;quot;control-plane-kubeadm&amp;quot;, &amp;quot;infrastructure-docker&amp;quot;]
}
EOF
cmd/clusterctl/hack/local-overrides.py
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You should be able to see the generated manifests at &lt;code&gt;~/..cluster-api/overrides/infrastructure-docker/latest/infrastructure-components.yaml&lt;/code&gt;, the only last thing we need to do is let clusterctl know where to find them:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cat &amp;gt; ~/.cluster-api/clusterctl.yaml &amp;lt;&amp;lt;EOF
providers:
  - name: docker
    url: $HOME/.cluster-api/overrides/infrastructure-docker/latest/infrastructure-components.yaml
    type: InfrastructureProvider
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, we can use the  &lt;code&gt;clusterctl init&lt;/code&gt; command printed by the &lt;code&gt;local-verrides.py&lt;/code&gt; script to create all CAPI and CAPD components inside our kind cluster:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;clusterctl init --core cluster-api:v0.3.0 --bootstrap kubeadm:v0.3.0 --control-plane kubeadm:v0.3.0 --infrastructure docker:v0.3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this stage, we should see the following deployments created and ready (1/1).&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;k get deploy -A | grep cap
capd-system                         capd-controller-manager                         1/1
capi-kubeadm-bootstrap-system       capi-kubeadm-bootstrap-controller-manager       1/1
capi-kubeadm-control-plane-system   capi-kubeadm-control-plane-controller-manager   1/1
capi-system                         capi-controller-manager                         1/1
capi-webhook-system                 capi-controller-manager                         1/1 
capi-webhook-system                 capi-kubeadm-bootstrap-controller-manager       1/1
capi-webhook-system                 capi-kubeadm-control-plane-controller-manager   1/1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If &lt;code&gt;capd-system&lt;/code&gt; deployment is not READY and stuck trying to pull the image, make sure that the &lt;code&gt;capd-controller-manager&lt;/code&gt; deployment is using the image that was generated in the previous section.&lt;/p&gt;

&lt;h2 id=&#34;generating-a-capd-managed-cluster-manifest&#34;&gt;Generating a CAPD-managed cluster manifest&lt;/h2&gt;

&lt;p&gt;All the instructions provided so far can also be found in the official documentation. However, at this stage, the book started having big gaps that were not trivial to figure out. TLDR: you can just run the below command to generate a sample capd cluster manifest and move on to the next section. However if you ever need to modify this command, check out my notes below it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DOCKER_POD_CIDRS=&amp;quot;192.168.0.0/16&amp;quot; \
DOCKER_SERVICE_CIDRS=&amp;quot;10.128.0.0/12&amp;quot; \
DOCKER_SERVICE_DOMAIN=&amp;quot;cluster.local&amp;quot; \
clusterctl config cluster capd --kubernetes-version v1.17.5 \
--from ./test/e2e/data/infrastructure-docker/cluster-template.yaml \
--target-namespace default \
--control-plane-machine-count=1 \
--worker-machine-count=1 \
&amp;gt; capd.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At the time of writing, CAPD used &lt;code&gt;kindest/node&lt;/code&gt; docker images (see &lt;code&gt;defaultImageName&lt;/code&gt; in test/infrastructure/docker/docker/machines.go) and combined it with a tag provided in the &lt;code&gt;--kubernetes-version&lt;/code&gt; argument. Be sure to always check if there&amp;rsquo;s a matching tag on &lt;a href=&#34;https://hub.docker.com/r/kindest/node/tags&#34; target=&#34;_blank&#34;&gt;dockerhub&lt;/a&gt;. If it is missing (e.g. v1.17.3), Machine controller will fail to create a docker container for your kubernetes cluster and you&amp;rsquo;ll only see the load-balancer container being created.&lt;/p&gt;

&lt;p&gt;Another issue is the clusterctl may not find the &lt;code&gt;cluster-template.yaml&lt;/code&gt; where it expects, so it would have to be provided with the &lt;code&gt;--from&lt;/code&gt; argument. This template would require additional variables (all that start with &lt;code&gt;DOCKER_&lt;/code&gt;) that have to be provided for it to be rendered. These can be modified as long as you understand what they do.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: never set the POD CIDR equal to the Service CIDR unless you want to spend your time troubleshooting networking and DNS issues.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finally, you should also make sure that the target namespace is specified explicitly, otherwise the generated manifest will contain an incorrect combination of namespaces and will get rejected by the validating webhook.&lt;/p&gt;

&lt;h2 id=&#34;creating-a-capd-managed-cluster&#34;&gt;Creating a CAPD-managed cluster&lt;/h2&gt;

&lt;p&gt;The final step is to apply the generated manifest and let the k8s controllers do their job.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl apply -f capd.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It&amp;rsquo;s worth spending a bit of time understanding what&amp;rsquo;s some of these controllers do. The &lt;code&gt;DockerCluster&lt;/code&gt; controller is responsible for the creation of a load-balancing container (capd-lb). A load-balancer is needed to provide a single API endpoint in front of multiple controller nodes. It&amp;rsquo;s built on top of the HAProxy image (kindest/haproxy:2.1.1-alpine), and does the healthchecking and load-balancing across all cluster controller nodes. It&amp;rsquo;s worth noting that the &lt;code&gt;DockerCluster&lt;/code&gt; resource is marked as &lt;code&gt;READY&lt;/code&gt; as soon as the controller can read the IP assigned to the &lt;code&gt;capd-lb&lt;/code&gt; container, which doesn&amp;rsquo;t necessarily reflect that the cluster itself is built.&lt;/p&gt;

&lt;p&gt;Typically, all nodes in a CAPI-managed clusters are bootstrapped with cloud-init that is generated by the bootstrap controller. However Docker doesn&amp;rsquo;t have a cloud-init equivalent, so the &lt;code&gt;DockerMachine&lt;/code&gt; controller simply executes each line of the bootstrap script using the &lt;code&gt;docker exec&lt;/code&gt; commands. It&amp;rsquo;s also worth noting that containers themselves are also managed using docker CLI and not API.&lt;/p&gt;

&lt;h2 id=&#34;installing-cni-and-metallb&#34;&gt;Installing CNI and MetalLB&lt;/h2&gt;

&lt;p&gt;As a bonus, I&amp;rsquo;ll show how to install CNI and MetalLB to build a completely functional k8s cluster. First, we need to extract the kubeconfig file and save it locally:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl get secret/capd-kubeconfig -o jsonpath={.data.value} \
  | base64 --decode  &amp;gt; ./capd.kubeconfig
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we can apply the CNI config, as suggested in the book.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;KUBECONFIG=./capd.kubeconfig kubectl \
  apply -f https://docs.projectcalico.org/v3.12/manifests/calico.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A minute later, both nodes should transition to &lt;code&gt;Ready&lt;/code&gt; state:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;KUBECONFIG=./capd.kubeconfig kubectl get nodes
NAME                              STATUS   ROLES    AGE   VERSION
capd-capd-control-plane-hn724     Ready    master   30m   v1.17.5
capd-capd-md-0-84df67c74b-lzm6z   Ready    &amp;lt;none&amp;gt;   29m   v1.17.5
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In order to be able to create load-balancer type services, we can install MetalLB in L2 mode. Thanks to the &lt;code&gt;docker.sock&lt;/code&gt; mounting we&amp;rsquo;ve done above, our test cluster is now attached to the same docker bridge as the rest of the containers in host OS. We can easily determine what subnet is being used by it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DOCKER_BRIDGE_SUBNET=$(docker network inspect bridge | jq -r &#39;.[0].IPAM.Config[0].Subnet&#39;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next, using the &lt;a href=&#34;http://jodies.de/ipcalc-archive/ipcalc-0.41/ipcalc&#34; target=&#34;_blank&#34;&gt;ipcalc&lt;/a&gt; tool, we can pick a small range from the high end of that subnet:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DOCKER_HIGHEND_RANGE=$(ipcalc -s 6 ${DOCKER_BRIDGE_SUBNET}  | grep 29 | tail -n 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we can create the configuration for MetalLB:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cat &amp;gt; metallb_cm.yaml &amp;lt;&amp;lt;EOF
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: my-ip-space
      protocol: layer2
      addresses:
      - $DOCKER_HIGHEND_RANGE   
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, all we have to do is install it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;KUBECONFIG=./capd.kubeconfig kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/namespace.yaml
KUBECONFIG=./capd.kubeconfig kubectl apply -f metallb_cm.yaml
KUBECONFIG=./capd.kubeconfig kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/metallb.yaml
KUBECONFIG=./capd.kubeconfig kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey=&amp;quot;$(openssl rand -base64 128)&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To test it, we can deploy a test application and expose it with a service of type LoadBalancer:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;KUBECONFIG=./capd.kubeconfig kubectl create deployment test --image=nginx
KUBECONFIG=./capd.kubeconfig kubectl expose deployment test --name=lb --port=80 --target-port=80 --type=LoadBalancer
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we should be able to access the application running inside the cluster by hitting the external load-balancer IP:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;MetalLB_IP=$(KUBECONFIG=./capd.kubeconfig kubectl get svc lb -o jsonpath=&#39;{.status.loadBalancer.ingress[0].ip}&#39;)
curl -s $MetalLB_IP | grep &amp;quot;Thank you&amp;quot;
&amp;lt;p&amp;gt;&amp;lt;em&amp;gt;Thank you for using nginx.&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/capd.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Network Simulations with Network Service Mesh</title>
      <link>https://networkop.co.uk/post/2020-01-nsm-topo/</link>
      <pubDate>Fri, 24 Jan 2020 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2020-01-nsm-topo/</guid>
      <description>

&lt;p&gt;In September 2019 I had the honour to &lt;a href=&#34;https://onseu19.sched.com/event/SYsb/large-scale-network-simulations-in-kubernetes-michael-kashin-arista-networks&#34; target=&#34;_blank&#34;&gt;present&lt;/a&gt; at Open Networking Summit in Antwerp. My talk was about &lt;a href=&#34;https://github.com/networkop/meshnet-cni&#34; target=&#34;_blank&#34;&gt;meshnet&lt;/a&gt; CNI plugin, &lt;a href=&#34;https://github.com/networkop/k8s-topo&#34; target=&#34;_blank&#34;&gt;k8s-topo&lt;/a&gt; orchestrator and how to use them for large-scale network simulations in Kubernetes. During the same conference, I attended a talk about Network Service Mesh and its new &lt;a href=&#34;https://onseu19.sched.com/event/SYum/kernel-based-forwarding-plane-for-network-service-mesh-radoslav-dimitrov-vmware&#34; target=&#34;_blank&#34;&gt;kernel-based forwarding dataplane&lt;/a&gt; which had a lot of similarities with the work that I&amp;rsquo;ve done for meshnet. Having had a chat with the presenters, we&amp;rsquo;ve decided that it would be interesting to try and implement a meshnet-like functionality with NSM. In this post, I&amp;rsquo;ll try to document some of the findings and results of my research.&lt;/p&gt;

&lt;h1 id=&#34;network-service-mesh-introduction&#34;&gt;Network Service Mesh Introduction&lt;/h1&gt;

&lt;p&gt;&lt;a href=&#34;https://networkservicemesh.io/&#34; target=&#34;_blank&#34;&gt;NSM&lt;/a&gt; is a CNCF project aimed at providing service mesh-like capabilities for L2/L3 traffic. In the context of Kubernetes, NSM&amp;rsquo;s role is to interconnect pods and setup the underlying forwarding, which involves creating new interfaces, allocating IPs and configuring pod&amp;rsquo;s routing table. The main use cases are cloud-native network functions (e.g. 5G), service function chaining and any containerised applications that may need to talk over non-standard protocols. Similar to traditional service meshes, the intended functionality is achieved by injecting sidecar containers that communicate with a distributed control plane of network service managers, deployed as a &lt;a href=&#34;https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/&#34; target=&#34;_blank&#34;&gt;daemonset&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I&amp;rsquo;ll try to avoid repeating NSM&amp;rsquo;s theory here and instead refer my readers to the official &lt;a href=&#34;https://networkservicemesh.io/docs/concepts/what-is-nsm&#34; target=&#34;_blank&#34;&gt;documentation&lt;/a&gt; and a very good introductory &lt;a href=&#34;https://docs.google.com/presentation/d/1IC2kLnQGDz1hbeO0rD7Y82O_4NwzgIoGgm0oOXyaQ9Y/edit#slide=id.p&#34; target=&#34;_blank&#34;&gt;slide deck&lt;/a&gt;. There are a few concepts, however, that are critical to the understanding of this blogpost, that I&amp;rsquo;ll mention here briefly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Network Services&lt;/strong&gt; are built around a client-server model - a client receives a service from an endpoint (server).&lt;/li&gt;
&lt;li&gt;Both client and endpoint are implemented as &lt;strong&gt;containers&lt;/strong&gt; and interact with &lt;strong&gt;local control plane agents&lt;/strong&gt; over a gRPC-based API.&lt;/li&gt;
&lt;li&gt;Typically, a &lt;strong&gt;client&lt;/strong&gt; would request a service with &lt;code&gt;ns.networkservicemesh.io&lt;/code&gt; &lt;strong&gt;annotation&lt;/strong&gt;, which gets matched by a mutating webhook responsible for injecting an init container.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Endpoints&lt;/strong&gt;, being designed specifically to provide network services, have endpoint container statically defined as a &lt;strong&gt;sidecar&lt;/strong&gt; (unless they natively implement NSM&amp;rsquo;s SDK).&lt;/li&gt;
&lt;li&gt;One important distinction between client and endpoint sidecars is that the former is an &lt;strong&gt;init&lt;/strong&gt; container (runs to completion at pod create time) and the latter is a normal &lt;strong&gt;sidecar&lt;/strong&gt; which allows service reconfiguration at runtime.&lt;/li&gt;
&lt;li&gt;All client and endpoint configurations get passed as &lt;strong&gt;environment variables&lt;/strong&gt; to the respective containers either dynamically (client) or statically (endpoint).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given all of the above, this is how you&amp;rsquo;d use NSM to create a point-to-point link between any two pods.&lt;/p&gt;

&lt;h1 id=&#34;using-nsm-to-create-links-between-pods&#34;&gt;Using NSM to create links between pods&lt;/h1&gt;

&lt;p&gt;First, we need to decide which side of the link will be a client and which will be an endpoint. This is where we&amp;rsquo;ll abuse NSM&amp;rsquo;s concepts for the first time as it really doesn&amp;rsquo;t matter how this allocation takes place. For a normal network service, it&amp;rsquo;s fairly easy to identify and map client/server roles, however, for topology simulations they can be assigned arbitrarily as both sides of the link are virtually equivalent.&lt;/p&gt;

&lt;p&gt;The next thing we need to do is statically add sidecar containers not only to the endpoint side of the link but to the client as well. This is another abuse of NSM&amp;rsquo;s intended mode of operation, where a client init container gets injected automatically by the webhook. The reason for that is that the init container will block until its network service request gets accepted, which may create a circular dependency if client/endpoint roles are assigned arbitrarily, as discussed above.&lt;/p&gt;

&lt;p&gt;The resulting &amp;ldquo;endpoint&amp;rdquo; side of the link will have the following pod manifest. The NSE sidecar container will read the environment variables and use NSM&amp;rsquo;s &lt;a href=&#34;https://github.com/networkservicemesh/networkservicemesh/tree/master/sdk&#34; target=&#34;_blank&#34;&gt;SDK&lt;/a&gt; to register itself with a &lt;code&gt;p2p&lt;/code&gt; network service with a &lt;code&gt;device=device-2&lt;/code&gt; label.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: device-2
spec:
  containers:
  - image: alpine:latest
    command: [&amp;quot;tail&amp;quot;, &amp;quot;-f&amp;quot;, &amp;quot;/dev/null&amp;quot;]
    name: alpine
  - name: nse-sidecar
    image: networkservicemesh/topology-sidecar-nse:master
    env:
    - name: ENDPOINT_NETWORK_SERVICE
      value: &amp;quot;p2p&amp;quot;
    - name: ENDPOINT_LABELS
      value: &amp;quot;device=device-2&amp;quot;
    - name: IP_ADDRESS
      value: &amp;quot;10.60.1.0/24&amp;quot;
    resources:
      limits:
        networkservicemesh.io/socket: &amp;quot;1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When a local control plane agent receives the above registration request, it will create a new k8s &lt;code&gt;NetworkServiceEndpoint&lt;/code&gt; resource, effectively letting all the other agents know where to find this particular service endpoint (in this case it&amp;rsquo;s the k8s node called &lt;code&gt;nsm-control-plane&lt;/code&gt;). Note that the below resource is managed by NSM&amp;rsquo;s control plane and should not be created by the user:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networkservicemesh.io/v1alpha1
kind: NetworkServiceEndpoint
metadata:
  generateName: p2p
  labels:
    device: device-2
    networkservicename: p2p
  name: p2ppdp2d
spec:
  networkservicename: p2p
  nsmname: nsm-control-plane
  payload: IP
status:
  state: RUNNING
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The next bit is the manifest of the network service itself. Its goal is to establish a relationship between multiple clients and endpoints of a service by matching their network service labels.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networkservicemesh.io/v1alpha1
kind: NetworkService
metadata:
  name: p2p
spec:
  matches:
  - match: 
    sourceSelector:
      link: net-0
    route:
    - destination: 
      destinationSelector:
        device: device-2
  payload: IP
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The final bit is the &amp;ldquo;client&amp;rdquo; side of the link which will have the following pod manifest. Note that the format of &lt;code&gt;NS_NETWORKSERVICEMESH_IO&lt;/code&gt; variable is the same as the one used in &lt;a href=&#34;https://github.com/networkservicemesh/networkservicemesh/blob/master/docs/spec/admission.md#what-to-trigger-on&#34; target=&#34;_blank&#34;&gt;annotations&lt;/a&gt; and can be read as &amp;ldquo;client requesting a &lt;code&gt;p2p&lt;/code&gt; service with two labels (&lt;code&gt;link=net-0&lt;/code&gt; and &lt;code&gt;peerif=eth21&lt;/code&gt;) and wants to connect to it over a local interface called &lt;code&gt;eth12&lt;/code&gt;&amp;rdquo;.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: device-1
spec:
  containers:
  - image: alpine:latest
    command: [&amp;quot;tail&amp;quot;, &amp;quot;-f&amp;quot;, &amp;quot;/dev/null&amp;quot;]
    name: alpine
  - name: nsc-sidecar
    image: networkservicemesh/topology-sidecar-nsc:master
    env:
    - name: NS_NETWORKSERVICEMESH_IO
      value: p2p/eth12?link=net-0&amp;amp;peerif=eth21
    resources:
      limits:
        networkservicemesh.io/socket: &amp;quot;1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The client&amp;rsquo;s sidecar will read the above environment variable and send a connection request to the local control plane agent which will perform the following sequence of steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Locate a network service called &lt;code&gt;p2p&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Find a match based on client-provided labels (&lt;code&gt;link=net-0&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Try to find a matching network service endpoint (&lt;code&gt;device=device-2&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Contact the remote agent hosting a matching endpoint (found in NSE CRDs) and relay the connection request.&lt;/li&gt;
&lt;li&gt;If the request gets accepted by the endpoint, instruct the local forwarding agent to set up pod&amp;rsquo;s networking.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&#34;topology-orchestration-with-k8s-topo&#34;&gt;Topology orchestration with k8s-topo&lt;/h1&gt;

&lt;p&gt;Looking at the above manifests, it&amp;rsquo;s clear that writing them manually, even for smaller topologies, can be a serious burden. That&amp;rsquo;s why I&amp;rsquo;ve adapted the &lt;a href=&#34;https://github.com/networkop/k8s-topo&#34; target=&#34;_blank&#34;&gt;k8s-topo&lt;/a&gt; tool that I&amp;rsquo;ve written originally for &lt;a href=&#34;https://github.com/networkop/meshnet-cni&#34; target=&#34;_blank&#34;&gt;meshnet-cni&lt;/a&gt; to produce and instantiate NSM-compliant manifest based on a single light-weight topology YAML file. The only thing that&amp;rsquo;s needed to make it work with NSM is to add a &lt;code&gt;nsm: true&lt;/code&gt; to the top of the file, e.g.:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;nsm: true
links:
  - endpoints: [&amp;quot;device-1:eth12&amp;quot;, &amp;quot;device-2:eth21&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Behind the scenes, k8s-topo will create the required network service manifest and configure all pods with correct sidecars and variables. As an added bonus, it will still attempt to inject startup configs and expose ports as described &lt;a href=&#34;https://github.com/networkop/k8s-topo&#34; target=&#34;_blank&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/k8s-nsm.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;h1 id=&#34;nsm-vs-meshnet-for-network-simulations&#34;&gt;NSM vs Meshnet for network simulations&lt;/h1&gt;

&lt;p&gt;In the context of virtual network simulations, both NSM and meshnet-cni can perform similar functions, however, their implementation and modes of operation are rather different. Here are the main distinctions of a CNI plugin approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All networking is setup BEFORE the pod is started.&lt;/li&gt;
&lt;li&gt;CNI plugin does all the work so there&amp;rsquo;s no need for sidecar containers.&lt;/li&gt;
&lt;li&gt;A very thin code base for a very specific use case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here are some of the distinctions of an NSM-based approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All networking is setup AFTER the pod is started.&lt;/li&gt;
&lt;li&gt;This does come with a requirement for a sidecar container, but potentially allows for runtime reconfiguration.&lt;/li&gt;
&lt;li&gt;No requirement for a CNI plugin at all.&lt;/li&gt;
&lt;li&gt;More generic use cases are possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end, none of the options limit the currently available featureset of k8s-topo and the choice can be done based on the characteristics of an individual environment, e.g. if it&amp;rsquo;s a managed k8s from GCP (GKE) or Azure (AKS) then most likely you&amp;rsquo;ll be running &lt;a href=&#34;https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#kubenet&#34; target=&#34;_blank&#34;&gt;kubenet&lt;/a&gt; and won&amp;rsquo;t have an option to install any CNI plugin at all, in which case NSM can be the only available solution.&lt;/p&gt;

&lt;h1 id=&#34;demo&#34;&gt;Demo&lt;/h1&gt;

&lt;p&gt;Now it&amp;rsquo;s demo time and I&amp;rsquo;ll show how to use k8s-topo together with NSM to build a 10-node virtual router topology. We start by spinning up a local &lt;a href=&#34;https://github.com/kubernetes-sigs/kind&#34; target=&#34;_blank&#34;&gt;kind&lt;/a&gt; kubernetes cluster and installing NSM on it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git clone https://github.com/networkservicemesh/networkservicemesh
cd networkservicemesh
make helm-init
SPIRE_ENABLED=false INSECURE=true FORWARDING_PLANE=kernel make helm-install-nsm 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next, we install the k8s-topo deployment and connect to the pod running it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl create -f https://raw.githubusercontent.com/networkop/k8s-topo/master/manifest.yml
kubectl exec -it deploy/k8s-topo -- sh
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For demonstration purposes I&amp;rsquo;ll use a random 10-node tree topology generated using a &lt;a href=&#34;https://en.wikipedia.org/wiki/Loop-erased_random_walk&#34; target=&#34;_blank&#34;&gt;loop-erased random walk&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;./examples/builder/builder 10 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The only thing needed to make it work with NSM is set the &lt;code&gt;nsm&lt;/code&gt; flag to &lt;code&gt;true&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sed -i &#39;$ a\nsm: true&#39; ./examples/builder/random.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now everything&amp;rsquo;s ready for us to instantiate the topology inside k8s:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;k8s-topo --create ./examples/builder/random.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once all the pods are up, we can issue a ping from one of the routers to every other router in the topology and confirm the connectivity between their loopback IPs:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;for i in `seq 0 9`; do (kubectl exec qrtr-192-0-2-0 -c router -- ping -c 1 192.0.2.$i|grep loss); done

1 packets transmitted, 1 packets received, 0% packet loss
1 packets transmitted, 1 packets received, 0% packet loss
1 packets transmitted, 1 packets received, 0% packet loss
1 packets transmitted, 1 packets received, 0% packet loss
1 packets transmitted, 1 packets received, 0% packet loss
1 packets transmitted, 1 packets received, 0% packet loss
1 packets transmitted, 1 packets received, 0% packet loss
1 packets transmitted, 1 packets received, 0% packet loss
1 packets transmitted, 1 packets received, 0% packet loss
1 packets transmitted, 1 packets received, 0% packet loss
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you want to have a look at your topology, it&amp;rsquo;s possible to make k8s-topo generate a D3 graph of all pods and their connections and view it in the browser:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;k8s-topo --graph ./examples/builder/random.yml
INFO:__main__:D3 graph created
INFO:__main__:URL: http://172.17.0.3:30000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/k8s-nsm-topo.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Network-as-a-Service Part 3 - Authentication and Admission control</title>
      <link>https://networkop.co.uk/post/2019-06-naas-p3/</link>
      <pubDate>Thu, 27 Jun 2019 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2019-06-naas-p3/</guid>
      <description>

&lt;p&gt;In the previous two posts, we&amp;rsquo;ve seen how to &lt;a href=&#34;https://networkop.co.uk/post/2019-06-naas-p2/&#34;&gt;build&lt;/a&gt; a custom network API with Kubernetes CRDs and &lt;a href=&#34;https://networkop.co.uk/post/2019-06-naas-p1/&#34;&gt;push&lt;/a&gt; the resulting configuration to network devices. In this post, we&amp;rsquo;ll apply the final touches by enabling oAuth2 authentication and enforcing separation between different tenants. All of these things are done while the API server processes incoming requests, so it would make sense to have a closer look at how it does that first.&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-request-admission-pipeline&#34;&gt;Kubernetes request admission pipeline&lt;/h2&gt;

&lt;p&gt;Every incoming request has to go through several stages before it can get accepted and persisted by the API server. Some of these stages are mandatory (e.g. authentication), while some can be added through webhooks. The following diagram comes from another &lt;a href=&#34;https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/&#34; target=&#34;_blank&#34;&gt;blogpost&lt;/a&gt; that covers each one of these stages in detail:&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/admission-controller-phases.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Specifically for NaaS platform, this is how we&amp;rsquo;ll use the above stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;All users will authenticate with Google and get mapped to individual namespace/tenant based on their google alias.&lt;/li&gt;
&lt;li&gt;Mutating webhook will be used to inject default values into each request and allow users to define ranges as well as individual ports.&lt;/li&gt;
&lt;li&gt;Object schema validation will do the syntactic validation of each request.&lt;/li&gt;
&lt;li&gt;Validating webhook will perform the semantic validation to make sure users cannot change ports assigned to a different tenant.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following sections will cover these stages individually.&lt;/p&gt;

&lt;h2 id=&#34;authenticating-with-google&#34;&gt;Authenticating with Google&lt;/h2&gt;

&lt;p&gt;Typically, external users are authenticated using X.509 certificates, however, lack of CRL or  OCSP support in Kubernetes creates a problem since lost or exposed certs cannot be revoked. One of the alternatives is to use &lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens&#34; target=&#34;_blank&#34;&gt;OpenID Connect&lt;/a&gt; which works on top of the OAuth 2.0 protocol and is supported by a few very big identity providers like Google, Microsoft and Salesforce. Although OIDC has its own shortcomings (read &lt;a href=&#34;https://blog.gini.net/frictionless-kubernetes-openid-connect-integration-f1c356140937&#34; target=&#34;_blank&#34;&gt;this blogpost&lt;/a&gt; for details), it is still often preferred over X.509.&lt;/p&gt;

&lt;p&gt;In order to authenticate users with OIDC, we need to do three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure the API server to bind different user aliases to their respective tenants.&lt;/li&gt;
&lt;li&gt;Authenticate with the identity provider and get a signed token.&lt;/li&gt;
&lt;li&gt;Update local credentials to use this token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first step is pretty straightforward and can be done with a simple RBAC &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-3/oidc/manifest.yaml&#34; target=&#34;_blank&#34;&gt;manifest&lt;/a&gt;. The latter two steps can either be done manually or automatically with the help of &lt;a href=&#34;https://github.com/gini/dexter&#34; target=&#34;_blank&#34;&gt;dexter&lt;/a&gt;. NaaS Github repo contains a sample two-liner &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-3/dexter-auth-public.sh&#34; target=&#34;_blank&#34;&gt;bash script&lt;/a&gt; that uses dexter to authenticate with Google and save the token in the local &lt;code&gt;~/.kube/config&lt;/code&gt; file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All that&amp;rsquo;s required from a NaaS administrator is to maintain an up-to-date tenant role bindings and users can authenticate and maintain their tokens independently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;mutating-incoming-requests&#34;&gt;Mutating incoming requests&lt;/h2&gt;

&lt;p&gt;Mutating webhooks are commonly used to inject additional information (a sidecar proxy for service meshes) or defaults values (default CPU/memory) into incoming requests. Both mutating and validating webhooks get triggered based on a set of &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-3/webhooks/template-webhook.yaml&#34; target=&#34;_blank&#34;&gt;rules&lt;/a&gt; that match the API group and type of the incoming request. If there&amp;rsquo;s a match, a webhook gets called by the API server with an HTTP POST request containing the full body of the original request. The NaaS mutating &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-3/webhooks/mutate.py&#34; target=&#34;_blank&#34;&gt;webhook&lt;/a&gt; is written in Python/Flask and the first thing it does is extract the payload and its type:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;request_info = request.json
modified_spec = copy.deepcopy(request_info)
workload_type = modified_spec[&amp;quot;request&amp;quot;][&amp;quot;kind&amp;quot;][&amp;quot;kind&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next, we set the default values and normalize ports:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;if workload_type == &amp;quot;Interface&amp;quot;:
    defaults = get_defaults()
    set_intf_defaults(modified_spec[&amp;quot;request&amp;quot;][&amp;quot;object&amp;quot;][&amp;quot;spec&amp;quot;], defaults)
    normalize_ports(modified_spec[&amp;quot;request&amp;quot;][&amp;quot;object&amp;quot;][&amp;quot;spec&amp;quot;])
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The last function expands interface ranges, i.e. translates &lt;code&gt;1-5&lt;/code&gt; into &lt;code&gt;1,2,3,4,5&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;for port in ports:
    if not &amp;quot;-&amp;quot; in port:
        result.append(str(port))
    else:
        start, end = port.split(&amp;quot;-&amp;quot;)
        for num in range(int(start), int(end) + 1):
            result.append(str(num))  
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, we generate a json patch from the diff between the original and the mutated request, build a response and send it back to the API server.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;patch = jsonpatch.JsonPatch.from_diff(
    request_info[&amp;quot;request&amp;quot;][&amp;quot;object&amp;quot;], modified_spec[&amp;quot;request&amp;quot;][&amp;quot;object&amp;quot;]
)
admission_response = {
    &amp;quot;allowed&amp;quot;: True,
    &amp;quot;uid&amp;quot;: request_info[&amp;quot;request&amp;quot;][&amp;quot;uid&amp;quot;],
    &amp;quot;patch&amp;quot;: base64.b64encode(str(patch).encode()).decode(),
    &amp;quot;patchtype&amp;quot;: &amp;quot;JSONPatch&amp;quot;,
}
return jsonify(admissionReview = {&amp;quot;response&amp;quot;: admission_response})
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;a href=&#34;https://kubernetes.io/blog/2019/06/19/kubernetes-1-15-release-announcement/&#34; target=&#34;_blank&#34;&gt;latest&lt;/a&gt; (v1.15) release of Kubernetes has added support for default values to be defined inside the OpenAPI validation schema, making the job of writing mutating webhooks a lot easier.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;validating-incoming-requests&#34;&gt;Validating incoming requests&lt;/h2&gt;

&lt;p&gt;As we&amp;rsquo;ve seen in the &lt;a href=&#34;https://networkop.co.uk/post/2019-06-naas-p2/&#34;&gt;previous post&lt;/a&gt;, it&amp;rsquo;s possible to use OpenAPI schema to perform syntactic validation of incoming requests, i.e. check the structure and the values of payload variables. This function is very similar to what you can &lt;a href=&#34;http://plajjan.github.io/validating-data-with-YANG/&#34; target=&#34;_blank&#34;&gt;accomplish&lt;/a&gt; with a YANG model and, in theory, OpenAPI schema can be converted to YANG and &lt;a href=&#34;http://ipengineer.net/2018/10/yang-openapi-swagger-code-generation/&#34; target=&#34;_blank&#34;&gt;vice versa&lt;/a&gt;. However useful, such validation only takes into account a single input and cannot cross-correlate this data with other sources. In our case, the main goal is to protect one tenant&amp;rsquo;s data from being overwritten by request coming from another tenant. In Kubernetes, semantic validation is commonly done using &lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#validatingadmissionwebhook&#34; target=&#34;_blank&#34;&gt;validating&lt;/a&gt; admission webhooks and one of the most interesting tools in this landscape is &lt;a href=&#34;https://www.openpolicyagent.org/docs/v0.10.7/kubernetes-admission-control/&#34; target=&#34;_blank&#34;&gt;Open Policy Agent&lt;/a&gt; and its policy language called Rego.&lt;/p&gt;

&lt;h4 id=&#34;using-opa-s-policy-language&#34;&gt;Using OPA&amp;rsquo;s policy language&lt;/h4&gt;

&lt;p&gt;Rego is a special-purpose DSL with &amp;ldquo;rich support for traversing nested documents&amp;rdquo;. What this means is that it can iterate over dictionaries and lists without using traditional for loops. When it encounters an iterable data structure, it will automatically expand it to include all of its possible values. I&amp;rsquo;m not going to try to explain how &lt;a href=&#34;https://www.openpolicyagent.org/docs/v0.10.7/how-does-opa-work/&#34; target=&#34;_blank&#34;&gt;opa works&lt;/a&gt; in this post, instead I&amp;rsquo;ll show how to use it to solve our particular problem. Assuming that an incoming request is stored in the &lt;code&gt;input&lt;/code&gt; variable and &lt;code&gt;devices&lt;/code&gt; contain all custom device resources, this is how a Rego policy would look like:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;input.request.kind.kind == &amp;quot;Interface&amp;quot;
new_tenant := input.request.namespace
port := input.request.object.spec.services[i].ports[_]
new_device := input.request.object.spec.services[i].devicename
existing_device_data := devices[_][lower(new_device)].spec
other_tenant := existing_device_data[port].annotations.namespace
not new_tenant == other_tenant
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-3/webhooks/validate.rego&#34; target=&#34;_blank&#34;&gt;actual policy&lt;/a&gt; contains more than 7 lines but the most important ones are listed above and perform the following sequence of actions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify that the incoming request is of kind &lt;code&gt;Interface&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Extract its namespace and save it in the &lt;code&gt;new_tenant&lt;/code&gt; variable&lt;/li&gt;
&lt;li&gt;Save all ports in the &lt;code&gt;port&lt;/code&gt; variable&lt;/li&gt;
&lt;li&gt;Remember which device those ports belong to in the &lt;code&gt;new_device&lt;/code&gt; variables&lt;/li&gt;
&lt;li&gt;Extract existing port allocation information for each one of the above devices&lt;/li&gt;
&lt;li&gt;If any of the ports from the incoming request is found in the existing data, record its owner&amp;rsquo;s namespace&lt;/li&gt;
&lt;li&gt;Deny the request if the requesting port owner (tenant) is different from the current tenant.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Although Rego may not be that easy to write (or debug), it&amp;rsquo;s very easy to read, compared to an equivalent implemented in, say, Python, which would have taken x3 the number of lines and contain multiple for loops and conditionals. Like any DSL, it strives to strike a balance between readability and flexibility, while abstracting away less important things like web server request parsing and serialising.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The same functionality can be implemented in any standard web server (e.g. Python+Flask), so using OPA is not a requirement&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;demo&#34;&gt;Demo&lt;/h2&gt;

&lt;p&gt;This is a complete end-to-end demo of Network-as-a-Service platform and encompasses all the demos from the previous posts. The code for this demo is available &lt;a href=&#34;https://github.com/networkop/network-as-a-service/archive/part-3.zip&#34; target=&#34;_blank&#34;&gt;here&lt;/a&gt; and can be run on any Linux OS with Docker.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/naas-p3.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;h4 id=&#34;0-prepare-for-oidc-authentication&#34;&gt;0. Prepare for OIDC authentication&lt;/h4&gt;

&lt;p&gt;For this demo, I&amp;rsquo;ll only use a single non-admin user. Before you run the rest of the steps, you need to make sure you&amp;rsquo;ve followed &lt;a href=&#34;https://github.com/gini/dexter&#34; target=&#34;_blank&#34;&gt;dexter&lt;/a&gt; to setup google credentials and update OAuth client and user IDs in &lt;code&gt;kind.yaml&lt;/code&gt;, &lt;code&gt;dexter-auth.sh&lt;/code&gt; and &lt;code&gt;oidc/manifest.yaml&lt;/code&gt; files.&lt;/p&gt;

&lt;h4 id=&#34;1-build-the-test-topology&#34;&gt;1. Build the test topology&lt;/h4&gt;

&lt;p&gt;This step assumes you have &lt;a href=&#34;https://github.com/networkop/docker-topo&#34; target=&#34;_blank&#34;&gt;docker-topo&lt;/a&gt; installed and c(vEOS) image &lt;a href=&#34;https://github.com/networkop/docker-topo/tree/master/topo-extra-files/veos&#34; target=&#34;_blank&#34;&gt;built&lt;/a&gt; and available in local docker registry.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make topo
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This test topology can be any Arista EOS device reachable from the localhost. If using a different test topology, be sure to update the &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-3/topo/inventory.yaml&#34; target=&#34;_blank&#34;&gt;inventory&lt;/a&gt; file.&lt;/p&gt;

&lt;h4 id=&#34;2-build-the-kubernetes-cluster&#34;&gt;2. Build the Kubernetes cluster&lt;/h4&gt;

&lt;p&gt;The following step will build a docker-based &lt;a href=&#34;https://github.com/kubernetes-sigs/kind&#34; target=&#34;_blank&#34;&gt;kind&lt;/a&gt; cluster with a single control plane and a single worker node.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make kubernetes
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;3-check-that-the-cluster-is-functional&#34;&gt;3. Check that the cluster is functional&lt;/h4&gt;

&lt;p&gt;The following step will build a base docker image and push it to dockerhub. It is assumed that the user has done &lt;code&gt;docker login&lt;/code&gt; and has his username saved in the &lt;code&gt;DOCKERHUB_USER&lt;/code&gt; environment variable.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;export KUBECONFIG=&amp;quot;$(kind get kubeconfig-path --name=&amp;quot;naas&amp;quot;)&amp;quot;
make warmup
kubectl get pod test
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is a 100MB image, so it may take a few minutes for test pod to transition from &lt;code&gt;ContainerCreating&lt;/code&gt; to &lt;code&gt;Running&lt;/code&gt;&lt;/p&gt;

&lt;h4 id=&#34;4-build-the-naas-platform&#34;&gt;4. Build the NaaS platform&lt;/h4&gt;

&lt;p&gt;The next command will install and configure both mutating and validating admission webhooks, the watcher and scheduler services and all of the required CRDs and configmaps.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make build
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;5-authenticate-with-google&#34;&gt;5. Authenticate with Google&lt;/h4&gt;

&lt;p&gt;Assuming all files from step 0 have been updated correctly, the following command will open a web browser and prompt you to select a google account to authenticate with.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;make oidc-build
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;From now on, you should be able to switch to your google-authenticated user like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl config use-context mk
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And back to the admin user like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl config use-context kubernetes-admin@naas
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;6-test&#34;&gt;6. Test&lt;/h4&gt;

&lt;p&gt;To demonstrate how everything works, I&amp;rsquo;m going to issue three API requests. The &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-3/crds/03_cr.yaml&#34; target=&#34;_blank&#34;&gt;first&lt;/a&gt; API request will set up a large range of ports on test switches.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl config use-context mk
kubectl apply -f crds/03_cr.yaml                 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-3/crds/04_cr.yaml&#34; target=&#34;_blank&#34;&gt;second&lt;/a&gt; API request will try to re-assign some of these ports to a different tenant and will get denied by the validating controller.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl config use-context kubernetes-admin@naas
kubectl apply -f crds/04_cr.yaml        
Error from server (Port 11@deviceA is owned by a different tenant: tenant-a (request request-001), Port 12@deviceA is owned by a different tenant: tenant-a (request request-001),
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-3/crds/05_cr.yaml&#34; target=&#34;_blank&#34;&gt;third&lt;/a&gt; API request will update some of the ports from the original request within the same tenant.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl config use-context mk
kubectl apply -f crds/05_cr.yaml                 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The following result can be observed on one of the switches:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;devicea#sh run int eth2-3
interface Ethernet2
   description request-002
   shutdown
   switchport trunk allowed vlan 100
   switchport mode trunk
   spanning-tree portfast
interface Ethernet3
   description request-001
   shutdown
   switchport trunk allowed vlan 10
   switchport mode trunk
   spanning-tree portfast
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;outro&#34;&gt;Outro&lt;/h2&gt;

&lt;p&gt;Currently, Network-as-a-Service platform is more of a proof-of-concept of how to expose parts of the device data model for end users to consume in a safe and controllable way. Most of it is built out of standard Kubernetes component and the total amount of Python code is under 1000 lines, while the code itself is pretty linear. I have plans to add more things like an SPA front-end, Git and OpenFaaS integration, however, I don&amp;rsquo;t want to invest too much time until I get some sense of external interest. So if this is something that you like and think you might want to try, ping me via social media and I&amp;rsquo;ll try to help get things off the ground.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Network-as-a-Service Part 2 - Designing a Network API</title>
      <link>https://networkop.co.uk/post/2019-06-naas-p2/</link>
      <pubDate>Thu, 20 Jun 2019 00:00:00 +0000</pubDate>
      
      <guid>https://networkop.co.uk/post/2019-06-naas-p2/</guid>
      <description>

&lt;p&gt;In the &lt;a href=&#34;https://networkop.co.uk/post/2019-06-naas-p1/&#34;&gt;previous post&lt;/a&gt;, we&amp;rsquo;ve examined the foundation of the Network-as-a-Service platform. A couple of services were used to build the configuration from data models and templates and push it to network devices using Nornir and Napalm. In this post, we&amp;rsquo;ll focus on the user-facing part of the platform. I&amp;rsquo;ll show how to expose a part of the device data model via a custom API built on top of Kubernetes and how to tie it together with the rest of the platform components.&lt;/p&gt;

&lt;h2 id=&#34;interacting-with-a-kubernetes-api&#34;&gt;Interacting with a Kubernetes API&lt;/h2&gt;

&lt;p&gt;There are two main ways to interact with a &lt;a href=&#34;https://kubernetes.io/docs/concepts/overview/kubernetes-api/&#34; target=&#34;_blank&#34;&gt;Kubernetes API&lt;/a&gt;: one using a &lt;a href=&#34;https://kubernetes.io/docs/reference/using-api/client-libraries/&#34; target=&#34;_blank&#34;&gt;client library&lt;/a&gt;, which is how NaaS services communicate with K8s internally, the other way is with a command line tool called &lt;code&gt;kubectl&lt;/code&gt;, which is intended to be used by humans. In either case, each API request is expected to contain at least the following fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;apiVersion&lt;/strong&gt; - all API resources are grouped and versioned to allow multiple versions of the same kind to co-exist at the same time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;kind&lt;/strong&gt; - defines the type of object to be created.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;metadata&lt;/strong&gt; - collection of request attributes like name, namespaces, labels etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;spec&lt;/strong&gt; - the actual payload of the request containing the attributes of the requested object.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In order to describe these fields in a concise and human-readable way, API requests are often written in YAML, which is why you&amp;rsquo;ll see a lot of YAML snippets throughout this post. You can treat each one of those snippets as a separate API call that can be applied to a K8s cluster using a &lt;code&gt;kubectl apply&lt;/code&gt; command.&lt;/p&gt;

&lt;h2 id=&#34;designing-a-network-interface-api&#34;&gt;Designing a Network Interface API&lt;/h2&gt;

&lt;p&gt;The structure and logic behind any user-facing API can be very customer-specific. Although the use-case I&amp;rsquo;m focusing on here is a very simple one, my goal is to demonstrate the idea which, if necessary, can be adapted to other needs and requirements. So let&amp;rsquo;s assume we want to allow end users to change access ports configuration of multiple devices and this is how a sample API request may look like:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: network.as.a.service/v1
kind: Interface
metadata:
  name: request-001
  namespace: tenant-a
spec:
  services:
    - devicename: deviceA
      ports: [&amp;quot;1&amp;quot;, &amp;quot;15&amp;quot;]
      vlan: 10
      trunk: yes
    - devicename: deviceB
      ports: [&amp;quot;1&amp;quot;,&amp;quot;10&amp;quot;, &amp;quot;11&amp;quot;]
      vlan: 110
      trunk: no
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There are a few things to note in the above request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every request will have a unique name per namespace (tenant).&lt;/li&gt;
&lt;li&gt;The main payload inside the &lt;code&gt;.spec&lt;/code&gt; property is a list of (VLAN) network services that need to be configured on network devices.&lt;/li&gt;
&lt;li&gt;Each element of the list contains the name of the device, list of ports and a VLAN number to be associated with them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let&amp;rsquo;s see what it takes to make Kubernetes &amp;ldquo;understand&amp;rdquo; this API.&lt;/p&gt;

&lt;h2 id=&#34;introducing-kubernetes-crds&#34;&gt;Introducing Kubernetes CRDs&lt;/h2&gt;

&lt;p&gt;API server is the main component of the control plane of a Kubernetes cluster. It receives all incoming requests, validates them, notifies the respective controllers and stores them in a database.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/k8s-api.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Apart from the APIs exposing a set of standard resources, there&amp;rsquo;s an ability to define &lt;a href=&#34;https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/&#34; target=&#34;_blank&#34;&gt;custom resources&lt;/a&gt; - user-defined data structures that an API server can accept and store. Custom resources are the main building blocks for a lot of platforms built on top of K8s and at the very least they allow users to store and retrieve some arbitrary YAML data.&lt;/p&gt;

&lt;p&gt;In order to be able to create a custom resource, we need to define it with a custom resource definition (CRD) object that would describe the name of the resource, the api group it belongs to and, optionally, the structure and values of the YAML data via OpenAPI v3 &lt;a href=&#34;https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject&#34; target=&#34;_blank&#34;&gt;schema&lt;/a&gt;. This is how a CRD for the above Interface API would look like:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: interfaces.network.as.a.service
spec:
  group: network.as.a.service
  versions:
  - name: v1
    served: true
    storage: true
  scope: Namespaced
  subresources:
    status: {}
  names:
    plural: interfaces
    singular: interface
    kind: Interface
    shortNames:
    - intf
  validation:
    openAPIV3Schema:
      required: [&amp;quot;spec&amp;quot;]
      properties:
        spec:
          required: [&amp;quot;services&amp;quot;]
          properties:
            services:
              type: array
              items: 
                type: object
                required: [&amp;quot;devicename&amp;quot;, &amp;quot;vlan&amp;quot;, &amp;quot;ports&amp;quot;]
                properties:
                  devicename: 
                    type: string
                  vlan:
                    type: integer
                    minimum: 1
                    maximum: 4094
                  ports:
                    type: array
                    items:
                      type: string
                  trunk:
                    type: boolean
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As soon as we &lt;code&gt;kubectl apply&lt;/code&gt; the above YAML, our API server will expose the &lt;code&gt;Interface&lt;/code&gt; API  for all external users to perform standard CRUD operations on, and store the results alongside other K8s resources in etcd datastore.&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-custom-controllers&#34;&gt;Kubernetes custom controllers&lt;/h2&gt;

&lt;p&gt;Custom resources, by themselves, do not provide any way to define a business logic of what to do with their data. This job is normally performed by Kubernetes controllers that &amp;ldquo;watch&amp;rdquo; events that happen to these resources and perform actions based on that. This tandem between custom controllers and CRDs is so common, it led to the creation of an &lt;a href=&#34;https://coreos.com/operators/&#34; target=&#34;_blank&#34;&gt;operator pattern&lt;/a&gt; and a whole &lt;a href=&#34;https://twitter.com/alexellisuk/status/1132755044313522176&#34; target=&#34;_blank&#34;&gt;slew&lt;/a&gt; of operator frameworks with languages ranging from Go to Ansible.&lt;/p&gt;

&lt;p&gt;However, as I&amp;rsquo;ve mentioned in the &lt;a href=&#34;https://networkop.co.uk/post/2019-06-naas-p1/&#34;&gt;previous post&lt;/a&gt;, sometimes using a framework does not give you any benefit and after having looked at some of the most popular ones, I&amp;rsquo;ve decided to settle on my own implementation which turned out to be a lot easier. In essence, all that&amp;rsquo;s required from a custom controller is to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Subscribe to events about a custom resource (via K8s API).&lt;/li&gt;
&lt;li&gt;Once an event is received, perform the necessary business logic.&lt;/li&gt;
&lt;li&gt;Update the resource status if required.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let&amp;rsquo;s see how these custom controllers are implemented inside the NaaS platform.&lt;/p&gt;

&lt;h2 id=&#34;naas-controller-architecture&#34;&gt;NaaS controller architecture&lt;/h2&gt;

&lt;p&gt;NaaS platform has a special &lt;strong&gt;watcher&lt;/strong&gt; service that implements all custom controller logic. Its main purpose is to process incoming &lt;code&gt;Interface&lt;/code&gt; API events and generate a device-centric interface data model based on them.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://networkop.co.uk/img/naas-p2.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Internally, the watcher service is built out of two distinct controllers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;interface-watcher&lt;/strong&gt; - listens to &lt;code&gt;Interface&lt;/code&gt; API events and updates a custom &lt;code&gt;Device&lt;/code&gt; resource that stores an aggregated device-centric view of all interface API requests received up to date. Once all the changes have been made, it updates the status of the request and notifies the scheduler about all the devices affected by this event.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;device-watcher&lt;/strong&gt; - listens to &lt;code&gt;Device&lt;/code&gt; API events and generates configmaps containing a device interface data model. These configmaps are then consumed by enforcers to build the access interface part of the total device configuration.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;interface-watcher-architecture&#34;&gt;Interface-watcher architecture&lt;/h2&gt;

&lt;p&gt;The main loop of the &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-2/watcher/interface-watcher.py&#34; target=&#34;_blank&#34;&gt;interface-watcher&lt;/a&gt; receives &lt;code&gt;Interface&lt;/code&gt; API events as they arrive and processes each network service individually:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;for network_service in event_object[&amp;quot;spec&amp;quot;][&amp;quot;services&amp;quot;]:
    results.append(
        process_service(event_metadata, network_service, action, defaults)
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For each service, depending on the type of the event, we either add, update or delete ports from the global device-centric model:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;device = get_or_create_device(device_name, defaults)
device_data = device[&amp;quot;spec&amp;quot;]
if action == &amp;quot;ADDED&amp;quot;:
    device_data = add_ports(
        network_service, device_data, resource_name, resource_namespace
    )
elif action == &amp;quot;DELETED&amp;quot;:
    device_data = delete_ports(network_service, device_data, resource_name)
elif action == &amp;quot;MODIFIED&amp;quot;:
    device_data = delete_all_ports(device_data, resource_name)
    device_data = add_ports(
        network_service, device_data, resource_name, resource_namespace
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For each of the added ports, we copy all settings from the original request and annotate it with metadata about its current owner and tenant:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;ports = origin.pop(&amp;quot;ports&amp;quot;)
for port in ports:
    destination[port] = dict()
    destination[port] = origin
    destination[port][&amp;quot;annotations&amp;quot;] = annotate(owner, namespace)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This results in the following custom &lt;code&gt;Device&lt;/code&gt; resource being created from the original &lt;code&gt;Interface&lt;/code&gt; API request:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: network.as.a.service/v1
kind: Device
metadata:
  name: devicea
  namespace: default
spec:
  &amp;quot;1&amp;quot;:
    annotations:
      namespace: tenant-a
      owner: request-001
      timestamp: &amp;quot;2019-06-19 22:09:02&amp;quot;
    trunk: true
    vlan: 10
  &amp;quot;15&amp;quot;:
    annotations:
      namespace: tenant-a
      owner: request-001
      timestamp: &amp;quot;2019-06-19 22:09:02&amp;quot;
    trunk: true
    vlan: 10
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As subsequent requests can add or overwrite port ownership information, metadata allows the controller to be selective about which ports to modify in order to not accidentally delete ports assigned to a different owner:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;new_destination = copy.deepcopy(destination)
for port in origin[&amp;quot;ports&amp;quot;]:
    if (port in destination) and (
        destination[port].get(&amp;quot;annotations&amp;quot;, {}).get(&amp;quot;owner&amp;quot;, &amp;quot;&amp;quot;) == owner
    ):
        log.debug(f&amp;quot;Removing port {port} from structured config&amp;quot;)
        new_destination.pop(port, None)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once the event has been processed, interface-watcher updates the device resource with the new values:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;device[&amp;quot;spec&amp;quot;] = device_data
update_device(device_name, device, defaults)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The last command triggers a MODIFIED event on the &lt;code&gt;Device&lt;/code&gt; CR and this is where the next controller kicks in.&lt;/p&gt;

&lt;h2 id=&#34;device-watcher-architecture&#34;&gt;Device-watcher architecture&lt;/h2&gt;

&lt;p&gt;The job of a &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-2/watcher/device-watcher.py&#34; target=&#34;_blank&#34;&gt;device-watcher&lt;/a&gt; is to, first, extract the payload from the above request:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;event_object = event[&amp;quot;object&amp;quot;]
event_metadata = event_object[&amp;quot;metadata&amp;quot;
device_name = event_metadata[&amp;quot;name&amp;quot;]
device_data = event_object[&amp;quot;spec&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The payload is then serialised into a string and saved as a configmap with additional pointers to Jinja template and order/priority number to help the enforcer build the full device configuration:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;k8s_api = client.CoreV1Api()
body = {
    &amp;quot;metadata&amp;quot;: {
        &amp;quot;name&amp;quot;: device_name,
        &amp;quot;annotations&amp;quot;: {&amp;quot;order&amp;quot;: &amp;quot;99&amp;quot;, &amp;quot;template&amp;quot;: &amp;quot;interface.j2&amp;quot;},
        &amp;quot;labels&amp;quot;: {&amp;quot;device&amp;quot;: device_name, &amp;quot;app&amp;quot;: &amp;quot;naas&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;model&amp;quot;},
    },
    &amp;quot;data&amp;quot;: {&amp;quot;structured-config&amp;quot;: yaml.safe_dump(device_data)},
}

k8s_api.replace_namespaced_config_map(
    device_name, event_metadata[&amp;quot;namespace&amp;quot;], body
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The remaining part of the workflow is similar to what was described in the previous post. The scheduler receives the request with the list of devices to be re-provisioned, spins up the required number of enforcers who collect all relevant data models, combine them with Jinja templates and push the new config.&lt;/p&gt;

&lt;h2 id=&#34;demo&#34;&gt;Demo&lt;/h2&gt;

&lt;p&gt;This demo will pick up from where the previous one has left off. The assumption is that the test topology, K8s cluster and scheduler/enforcer services are already deployed as described in the &lt;a href=&#34;https://networkop.co.uk/post/2019-06-naas-p1/&#34;&gt;previous post&lt;/a&gt;. The code for this demo can be downloaded &lt;a href=&#34;https://github.com/networkop/network-as-a-service/archive/part-2.zip&#34; target=&#34;_blank&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&#34;deploy-the-watcher-service&#34;&gt;Deploy the watcher service&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;make watcher-build
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The above command performs the following actions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-2/crds/00_namespace.yaml&#34; target=&#34;_blank&#34;&gt;two namespaces&lt;/a&gt; that will represent different platform tenants&lt;/li&gt;
&lt;li&gt;Creates &lt;code&gt;Interface&lt;/code&gt; and &lt;code&gt;Device&lt;/code&gt; &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-2/crds/01_crd.yaml&#34; target=&#34;_blank&#34;&gt;CRD objects&lt;/a&gt; describing our custom APIs&lt;/li&gt;
&lt;li&gt;Deploys both watcher &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-2/watcher/manifest.yaml&#34; target=&#34;_blank&#34;&gt;custom controllers&lt;/a&gt; along with the necessary RBAC rules&lt;/li&gt;
&lt;li&gt;Uploads the interface &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-2/templates/interface.j2&#34; target=&#34;_blank&#34;&gt;jinja template&lt;/a&gt; to be used by enforcers&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;test&#34;&gt;Test&lt;/h2&gt;

&lt;p&gt;Issue the &lt;a href=&#34;https://github.com/networkop/network-as-a-service/blob/part-2/crds/03_cr.yaml&#34; target=&#34;_blank&#34;&gt;first&lt;/a&gt; &lt;code&gt;Interface&lt;/code&gt; API call:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl apply -f crds/03_cr.yaml         
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Check the logs of the interface-watcher to make sure it&amp;rsquo;s picked up the &lt;code&gt;Interface&lt;/code&gt; ADDED event:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl logs deploy/interface-watcher
2019-06-20 08:20:01 INFO interface-watcher - interface_watcher: Watching Interface CRDs
2019-06-20 08:20:09 INFO interface-watcher - process_services: Received ADDED event request-001 of Interface kind
2019-06-20 08:20:09 INFO interface-watcher - process_service: Processing ADDED config for Vlans 10 on device devicea
2019-06-20 08:20:09 INFO interface-watcher - get_device: Reading the devicea device resource
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Check the logs of the device-watcher to make sure it has detected the &lt;code&gt;Device&lt;/code&gt; API event:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl logs deploy/device-watcher
2019-06-20 08:20:09 INFO device-watcher - update_configmaps: Updating ConfigMap for devicea
2019-06-20 08:20:09 INFO device-watcher - update_configmaps: Creating configmap for devicea
2019-06-20 08:20:09 INFO device-watcher - update_configmaps: Configmap devicea does not exist yet. Creating
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Check the logs of the scheduler service to see if it has been notified about the change:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl logs deploy/scheduler
2019-06-20 08:20:09 INFO scheduler - webhook: Got incoming request from 10.32.0.4
2019-06-20 08:20:09 INFO scheduler - webhook: Request JSON payload {&#39;devices&#39;: [&#39;devicea&#39;, &#39;deviceb&#39;]}
2019-06-20 08:20:09 INFO scheduler - create_job: Creating job job-6rlwg0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Check the logs of the enforcer service to see if device configs have been generated and pushed:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl logs jobs/job-6rlwg0
2019-06-20 08:20:18 INFO enforcer - push_configs: Downloading Model configmaps
2019-06-20 08:20:18 INFO enforcer - get_configmaps: Retrieving the list of ConfigMaps matching labels {&#39;app&#39;: &#39;naas&#39;, &#39;type&#39;: &#39;model&#39;}
2019-06-20 08:20:18 INFO enforcer - push_configs: Found models: [&#39;devicea&#39;, &#39;deviceb&#39;, &#39;generic-cm&#39;]
2019-06-20 08:20:18 INFO enforcer - push_configs: Downloading Template configmaps
2019-06-20 08:20:18 INFO enforcer - get_configmaps: Retrieving the list of ConfigMaps matching labels {&#39;app&#39;: &#39;naas&#39;, &#39;type&#39;: &#39;template&#39;}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, we can check the result on the device itself:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;devicea#sh run int eth1
interface Ethernet1
   description request-001
   switchport trunk allowed vlan 10
   switchport mode trunk
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;coming-up&#34;&gt;Coming up&lt;/h2&gt;

&lt;p&gt;What we&amp;rsquo;ve covered so far is enough for end users to be able to modify access port settings on multiple devices via a standard API. However, there&amp;rsquo;s still nothing protecting the configuration created by one user from being overwritten by a request coming from a user in a different tenant. In the next post, I&amp;rsquo;ll show how to validate requests to make sure they do not cross the tenant boundaries. Additionally, I&amp;rsquo;ll show how to mutate incoming requests to be able to accept interface ranges and inject default values. To top it off, we&amp;rsquo;ll integrate NaaS with Google&amp;rsquo;s identity provider via OIDC to allow users to be mapped to different namespaces based on their google alias.&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
