<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>JSON on Digi Hunch</title><link>https://static.digihunch.com/tag/json/</link><description>Recent content in JSON on Digi Hunch</description><generator>Hugo -- gohugo.io</generator><language>en-US</language><lastBuildDate>Tue, 08 Apr 2025 14:19:11 -0400</lastBuildDate><atom:link href="https://static.digihunch.com/tag/json/index.xml" rel="self" type="application/rss+xml"/><item><title>Connect kubectl to private Kubernetes cluster in EKS and AKS</title><link>https://static.digihunch.com/2023/06/connect-kubectl-to-private-kubernetes-cluster-in-eks-and-aks/</link><pubDate>Sat, 10 Jun 2023 19:31:00 -0400</pubDate><guid>https://static.digihunch.com/2023/06/connect-kubectl-to-private-kubernetes-cluster-in-eks-and-aks/</guid><description>&lt;img src="https://static.digihunch.com/wp-content/uploads/2025/04/feature-kubectl-private-cluster.webp" alt="Featured image of post Connect kubectl to private Kubernetes cluster in EKS and AKS" /&gt;&lt;p class="wp-block-paragraph"&gt;Managed Kubernetes services give user a cluster endpoint and a number of worker nodes, with the choice. For each access, users have the choice of making them publicly available, or keeping them on private networking. In my opinion, any deployment beyond personal hobbies, should use Kubernetes private cluster, with both cluster endpoint and worker nodes on private subnet. There is no reason to expose computing nodes or Kubernetes management traffic publicly. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;For worker nodes, it is fairly easy to put VMs on private network, but many companies still have the cluster endpoint exposed publicly. There are usually two reasons. First, their CI/CD agent is hosted somewhere else on the Internet (instead of on private network with private connectivity to Kubernetes cluster) and need to access Kubernetes cluster endpoint. Second, when the cluster needs to connect with third-party identity provider as OIDC provider, a two-way communication is needed. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;There is a classic pattern of using a public bastion host (jump box), with a bastion host on the public subnet, routable to the private endpoint of managed Kubernetes service. Clients then connect to the bastion host via port 22 on a public IP address. The authentication is based on SSH key pair, or worse, password. The port forwarding (aka &lt;a href="https://www.ssh.com/academy/ssh/tunneling-example"&gt;SSH tunnelling&lt;/a&gt;) capability enables all the magics. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Exposing a jump box in the public subnet with RSA key authentication is still not favourable. In this post, I&amp;#8217;ll examine some secure patterns to connect to private endpoint with improved security posture. &lt;/p&gt;&#10;&lt;h2 class="wp-block-heading" id="h-aws-options"&gt;AWS options&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;There are two problems. First, how to establish connectivity to the Bastion host in a private subnet. Second, how to use the Bastion host to proxy traffic to the cluster endpoint also in private subnet. To the first problem, there are two potential solutions: SSM Session Manager, and EC2 Instance Connect (EIC) with EIC endpoint (EICE).&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;SSM Session Manager was introduce in 2018. It runs an agent on the EC2, which initiates a connection to the SSM endpoint on the AWS side. This connection enables not only Session Manager, but also other Systems Managers (SSM) services such as Fleet Manager, Patch Manager and State Manager. The problem that session manager originally addresses is server management.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;AWS launched EC2 Instance Connect (EIC) in 2019, and EIC Endpoint (EICE) in 2023. EIC addresses the problem with managing SSH key pairs at scale. It dynamically generates an SSH key pair for server access, based on IAM permission. However, it still requires an instance to have its SSH port publicly accessible. With EICE, it is no longer a requirement. In the &lt;a href="https://aws.amazon.com/blogs/compute/secure-connectivity-from-public-to-private-introducing-ec2-instance-connect-endpoint-june-13-2023/?utm_content=bufferfded7&amp;amp;utm_medium=social&amp;amp;utm_source=linkedin.com&amp;amp;utm_campaign=buffer"&gt;diagram&lt;/a&gt;, EICE is placed in a private subnet, allowing EICE service to reach private instances at their SSH port. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Here is a comparison of the two:&lt;/p&gt;&#10;&lt;figure class="wp-block-table is-style-stripes"&gt;&lt;table class="has-white-background-color has-background"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;EC2 Instance Connect (EIC) with EIC Endpoint&lt;/th&gt;&lt;th&gt;SSM Session Manager&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Location of Bastion host&lt;/td&gt;&lt;td&gt;Private Subnet.&lt;/td&gt;&lt;td&gt;Private Subnet&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Need Ingress Port&lt;/td&gt;&lt;td&gt;Yes. Port 22 must open to the endpoint.&lt;/td&gt;&lt;td&gt;No. SSM agent initiate outbound connection from the instance&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Traffic Path&lt;/td&gt;&lt;td&gt;AWS CLI → AWS EIC ES → EICE→EC2 Inst&lt;/td&gt;&lt;td&gt;AWS CLI → AWS SSM ES → SSM ← EC2 Inst&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Authentication&lt;/td&gt;&lt;td&gt;AWS IAM and ephemeral SSH key when using AWS CLI directly&lt;br&gt;AWS IAM and long-term SSH key when using SSH proxy command&lt;/td&gt;&lt;td&gt;AWS IAM and long-term SSH key when using AWS CLI directly or SSH proxy command&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Work with OpenSSH&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cost&lt;/td&gt;&lt;td&gt;There is no additional cost for using EIC.&lt;/td&gt;&lt;td&gt;No additional cost, unless private SSM Endpoint.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Let&amp;#8217;s take a look at each option.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;EC2 Instance Connect&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;To use EIC, pick an AMI that has it pre-installed and ensure instance profile has correct policy, as the document states &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-prerequisites.html"&gt;here&lt;/a&gt;. AWC CLI will make use of local OpenSSL client. So make sure there connection at port 22 is open. To make it work with EC2 instance on a private subnet, create an EC2 Instance Connect Endpoint on the VPC, and ensure that the security group of EC2 allows port 22 from the Endpoint. Run this command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ aws ec2-instance-connect ssh --instance-id i-00ea30a6e02db33fe&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;The command above simply generates a key pair internally, add the public key to the server side, and connect with SSH from the client side. The command takes you to an SSH session. Checking &lt;code&gt;ps -ef | grep ssh&lt;/code&gt; on the client machine, you can see the full parameter of SSH, including the location of the ephemeral private key. &lt;/p&gt;&#10;&lt;figure class="wp-block-image size-full"&gt;&lt;img loading="lazy" decoding="async" width="1024" height="95" src="https://static.digihunch.com/wp-content/uploads/2023/06/ssh-process.webp" alt="" class="wp-image-12921" srcset="https://static.digihunch.com/wp-content/uploads/2023/06/ssh-process.webp 1024w, https://static.digihunch.com/wp-content/uploads/2023/06/ssh-process-300x28.webp 300w, https://static.digihunch.com/wp-content/uploads/2023/06/ssh-process-768x71.webp 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;However, if you use AWS CLI open-tunnel as proxy command to ssh, then you&amp;#8217;d still have to use the key pair used to create the EC2 instance. As suggested at the bottom of &lt;a href="https://aws.amazon.com/blogs/compute/secure-connectivity-from-public-to-private-introducing-ec2-instance-connect-endpoint-june-13-2023/?utm_content=bufferfded7&amp;amp;utm_medium=social&amp;amp;utm_source=linkedin.com&amp;amp;utm_campaign=buffer"&gt;this&lt;/a&gt; blog post, the command is:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ ssh ec2-user@&lt;span style="color:#f92672"&gt;[&lt;/span&gt;INSTANCE&lt;span style="color:#f92672"&gt;]&lt;/span&gt; -i &lt;span style="color:#f92672"&gt;[&lt;/span&gt;SSH-KEY&lt;span style="color:#f92672"&gt;]&lt;/span&gt; -o ProxyCommand&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;aws ec2-instance-connect open-tunnel --instance-id %h&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;This is a bummer, because with native SSH tool you do not get the primary benefit of EIC &amp;#8211; ephemeral key pair. &lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;SSM Session Manager&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Now let&amp;#8217;s look at SSM session manager. Similarly, it needs an agent installed and &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-instance-permissions.html"&gt;IAM role&lt;/a&gt; configured. You can connect to from web console but more importantly, from AWS CLI:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ aws ssm start-session --target i-0531b19bec8ad022d&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;This command takes you to an SSH session with user &lt;code&gt;ssm-user&lt;/code&gt;, without starting an OpenSSH client process locally. User do not have to manage key pair. There is also a &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-enable-ssh-connections.html"&gt;document&lt;/a&gt; about using this command as proxy command, which uses an SSM document. I have one of the SSH config entry as:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;host i-* mi-*&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ProxyCommand sh -c &lt;span style="color:#e6db74"&gt;&amp;#34;aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters &amp;#39;portNumber=%p&amp;#39;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; User ec2-user&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IdentityFile ~/.ssh/id_rsa&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;This allows me to directly SSH to client using OpenSSL client (e.g. &lt;code&gt;ssh i-0531b19bec8ad022d&lt;/code&gt;) by Instance ID. With this, I also need to specify my own OS user and matching private key.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;I know I will use the OpenSSH client a lot from pipelines because it is very powerful. In both options, I have to live with managing key pairs myself. With SSM session manager&amp;#8217;s proxy command, the instance does not need port 22 to open, which is a great advantage, in terms of security and operation. SSM Session Manager is a winner.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;SOCKS5 proxy for kubectl&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Either SSM Session Manager or EIC with EICE enables an SSH tunnel with key encryption between client (a local computer or a pipeline agent). On top of the SSH tunnel, we can build a &lt;a href="https://en.wikipedia.org/wiki/SOCKS#SOCKS5"&gt;SOCKS5&lt;/a&gt; proxy. Kubernetes document has a good &lt;a href="https://kubernetes.io/docs/tasks/extend-kubernetes/socks5-proxy-access-api/"&gt;page&lt;/a&gt; on how to do this. I managed to get this to work with a few gotchas.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;&lt;svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="827px" viewBox="-0.5 -0.5 827 512" style="max-width:100%;max-height:512px;"&gt;&lt;defs&gt;&lt;linearGradient x1="0%" y1="100%" x2="0%" y2="0%" id="mx-gradient-f78e04-1-d05c17-1-s-0"&gt;&lt;stop offset="0%" style="stop-color: rgb(208, 92, 23); stop-opacity: 1;"/&gt;&lt;stop offset="100%" style="stop-color: rgb(247, 142, 4); stop-opacity: 1;"/&gt;&lt;/linearGradient&gt;&lt;style type="text/css"&gt;@import url(https://fonts.googleapis.com/css?family=Architects+Daughter);&amp;#xa;&lt;/style&gt;&lt;/defs&gt;&lt;g&gt;&lt;rect x="0" y="0" width="825" height="510" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/&gt;&lt;rect x="25" y="78.88" width="270" height="251.12" rx="25.11" ry="25.11" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" pointer-events="all"/&gt;&lt;rect x="435" y="60" width="370" height="240" rx="24" ry="24" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" pointer-events="all"/&gt;&lt;rect x="455" y="80" width="260" height="200" rx="20" ry="20" fill="#f5f5f5" stroke="#666666" stroke-width="2" pointer-events="all"/&gt;&lt;g fill="#333333" font-family="Helvetica" text-anchor="middle" font-size="12px"&gt;&lt;text x="584.5" y="97.5"&gt;Private Subnet&lt;/text&gt;&lt;/g&gt;&lt;rect x="475" y="123.75" width="120" height="130" fill="#fff2cc" stroke="#d6b656" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 131px; margin-left: 476px;"&gt;&lt;div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"&gt;&lt;div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"&gt;Bastion Host&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="535" y="143" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle"&gt;Bastion Host&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;rect x="75" y="110" width="190" height="193.75" fill="#fff2cc" stroke="#d6b656" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 188px; height: 1px; padding-top: 117px; margin-left: 76px;"&gt;&lt;div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"&gt;&lt;div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"&gt;Pipeline Agent&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="170" y="129" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle"&gt;Pipeline Agent&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;path d="M 351.25 68 C 351.25 66.34 353.49 65 356.25 65 C 357.58 65 358.85 65.32 359.79 65.88 C 360.72 66.44 361.25 67.2 361.25 68 L 361.25 309.5 C 361.25 311.16 359.01 312.5 356.25 312.5 C 353.49 312.5 351.25 311.16 351.25 309.5 Z" fill="#bac8d3" stroke="#23445d" stroke-miterlimit="10" transform="rotate(-90,356.25,188.75)" pointer-events="all"/&gt;&lt;path d="M 361.25 68 C 361.25 69.66 359.01 71 356.25 71 C 353.49 71 351.25 69.66 351.25 68" fill="none" stroke="#23445d" stroke-miterlimit="10" transform="rotate(-90,356.25,188.75)" pointer-events="all"/&gt;&lt;rect x="312.5" y="163.75" width="90" height="20" fill="none" stroke="none" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 174px; margin-left: 314px;"&gt;&lt;div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"&gt;&lt;div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"&gt;SSH Tunnel&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="358" y="177" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle"&gt;SSH Tunnel&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;path d="M 513.09 207.45 L 511.26 176.65 L 513.7 176.11 L 515.76 178.18 C 516.3 178.12 516.83 178.15 517.37 178.25 L 518.13 176.72 L 520.42 176.27 L 522.1 177.3 L 522.6 180.77 C 523.56 181.65 524.4 182.82 525.08 184.21 L 527.37 184.71 L 528.05 186.81 L 526.83 189.64 C 527.07 191.14 527.15 192.69 527.06 194.23 L 528.66 196.75 L 528.21 199.5 L 525.92 200.03 C 525.37 201.1 524.73 202.03 524.01 202.79 L 524.01 206.34 L 522.56 207.22 L 519.96 207.6 L 517.44 204.08 L 514.92 207.52 Z M 517.9 192.62 L 517.21 189.79 C 516.97 189.6 516.7 189.72 516.5 190.12 C 516.31 190.52 516.2 191.14 516.22 191.78 C 516.29 192.51 516.57 193.09 516.93 193.27 C 517.29 193.45 517.67 193.2 517.9 192.62 Z" fill="#697079" stroke="#697079" stroke-width="0.46" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;rect x="505" y="159.91" width="0" height="0" fill="none" stroke="none" pointer-events="all"/&gt;&lt;path d="M 529.43 191.24 L 527.6 160.44 L 530.04 159.91 L 532.1 161.97 C 532.63 161.92 533.17 161.94 533.7 162.05 L 534.47 160.52 L 536.76 160.06 L 538.44 161.09 L 538.93 164.57 C 539.89 165.45 540.74 166.62 541.41 168.01 L 543.7 168.51 L 544.39 170.61 L 543.17 173.44 C 543.41 174.93 543.49 176.48 543.4 178.02 L 545 180.55 L 544.54 183.3 L 542.25 183.83 C 541.71 184.9 541.06 185.83 540.34 186.58 L 540.34 190.14 L 538.89 191.02 L 536.3 191.4 L 533.78 187.88 L 531.26 191.32 Z M 534.24 176.42 L 533.55 173.59 C 533.31 173.4 533.04 173.52 532.84 173.92 C 532.64 174.32 532.53 174.94 532.56 175.58 C 532.63 176.31 532.9 176.89 533.26 177.07 C 533.62 177.25 534 177 534.24 176.42 Z" fill="#697079" stroke="#697079" stroke-width="0.46" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;path d="M 529.43 191.24 L 527.9 190.48 L 527.52 186.93 C 526.49 186.07 525.58 184.89 524.85 183.45 L 522.63 183.07 L 521.79 180.77 L 523.24 177.95 C 522.97 176.43 522.87 174.86 522.94 173.28 L 521.34 170.69 L 521.79 168.47 L 524.08 168.13 C 524.63 166.9 525.3 165.83 526.07 164.95 L 526.07 161.51 L 527.6 160.44 L 529.27 162.58 C 530.33 162.26 531.42 162.26 532.48 162.58 L 534.47 160.52 L 535.99 161.44 L 536.22 164.8 C 537.17 165.7 538 166.9 538.66 168.32 L 541.03 168.7 L 541.87 171.07 L 540.57 173.82 C 540.82 175.34 540.9 176.92 540.8 178.48 L 542.56 181.31 L 542.02 183.91 L 539.81 183.91 C 539.24 185.16 538.55 186.25 537.75 187.12 L 537.67 190.71 L 536.22 191.4 L 534.16 189.03 C 533.18 189.36 532.17 189.41 531.18 189.18 Z M 532.02 178.94 C 532.55 178.94 533.05 178.6 533.41 177.99 C 533.77 177.38 533.96 176.57 533.93 175.73 C 533.75 174.32 532.73 173.24 531.49 173.13 C 531 173.07 530.52 173.37 530.21 173.93 C 529.89 174.49 529.77 175.25 529.89 175.96 C 529.91 177.54 530.84 178.84 532.02 178.94" fill="#c0c6d2" stroke="#697079" stroke-width="0.46" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;path d="M 513.09 207.45 L 511.56 206.68 L 511.18 203.13 C 510.15 202.28 509.24 201.09 508.51 199.65 L 506.3 199.27 L 505.46 196.98 L 506.91 194.15 C 506.64 192.64 506.53 191.06 506.6 189.49 L 505 186.89 L 505.46 184.67 L 507.75 184.33 C 508.29 183.1 508.96 182.03 509.73 181.16 L 509.73 177.72 L 511.26 176.65 L 512.94 178.79 C 514 178.46 515.09 178.46 516.15 178.79 L 518.13 176.72 L 519.66 177.64 L 519.89 181 C 520.84 181.91 521.67 183.1 522.33 184.52 L 524.69 184.9 L 525.53 187.27 L 524.24 190.02 C 524.48 191.54 524.56 193.12 524.47 194.68 L 526.22 197.51 L 525.69 200.11 L 523.47 200.11 C 522.91 201.36 522.21 202.45 521.41 203.32 L 521.34 206.91 L 519.89 207.6 L 517.82 205.23 C 516.85 205.56 515.84 205.62 514.85 205.38 Z M 515.69 195.14 C 516.21 195.14 516.71 194.8 517.07 194.19 C 517.43 193.59 517.62 192.77 517.6 191.93 C 517.41 190.52 516.4 189.44 515.15 189.33 C 514.66 189.28 514.19 189.57 513.87 190.13 C 513.55 190.69 513.43 191.45 513.55 192.16 C 513.58 193.74 514.5 195.04 515.69 195.14" fill="#c0c6d2" stroke="#697079" stroke-width="0.46" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;path d="M 530.06 204.96 L 529.21 190.65 L 530.34 190.4 L 531.3 191.36 C 531.55 191.34 531.8 191.35 532.04 191.4 L 532.4 190.69 L 533.46 190.48 L 534.24 190.95 L 534.47 192.57 C 534.92 192.98 535.31 193.52 535.62 194.17 L 536.69 194.4 L 537.01 195.38 L 536.44 196.69 C 536.55 197.38 536.59 198.1 536.55 198.82 L 537.29 199.99 L 537.08 201.27 L 536.02 201.52 C 535.76 202.01 535.46 202.44 535.13 202.79 L 535.13 204.44 L 534.45 204.85 L 533.25 205.03 L 532.08 203.4 L 530.91 204.99 Z Z M 532.29 198.07 L 531.97 196.76 C 531.6 196.9 531.4 197.3 531.51 197.68 C 531.7 198.03 532 198.18 532.29 198.07 Z" fill="#697079" stroke="#697079" stroke-width="0.46" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;path d="M 530.06 204.96 L 529.35 204.6 L 529.17 202.95 C 528.69 202.56 528.27 202.01 527.93 201.34 L 526.9 201.16 L 526.51 200.09 L 527.18 198.78 C 527.06 198.08 527.01 197.35 527.04 196.62 L 526.3 195.41 L 526.51 194.38 L 527.57 194.22 C 527.83 193.65 528.14 193.15 528.5 192.75 L 528.5 191.15 L 529.21 190.65 L 529.98 191.65 C 530.48 191.49 530.98 191.49 531.47 191.65 L 532.4 190.69 L 533.11 191.11 L 533.21 192.68 C 533.66 193.1 534.04 193.65 534.35 194.31 L 535.45 194.49 L 535.84 195.59 L 535.23 196.87 C 535.35 197.57 535.38 198.31 535.34 199.03 L 536.16 200.34 L 535.91 201.55 L 534.88 201.55 C 534.61 202.13 534.29 202.64 533.92 203.04 L 533.89 204.71 L 533.21 205.03 L 532.26 203.93 C 531.8 204.09 531.33 204.11 530.87 204 Z M 531.26 199.24 C 531.54 199.3 531.82 199.16 532 198.85 C 532.18 198.55 532.24 198.13 532.15 197.75 C 532.03 197.04 531.56 196.54 531.02 196.54 C 530.8 196.55 530.59 196.7 530.44 196.95 C 530.3 197.2 530.24 197.53 530.27 197.86 C 530.32 198.57 530.73 199.15 531.26 199.24 Z" fill="#c0c6d2" stroke="#697079" stroke-width="0.46" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 215px; margin-left: 525px;"&gt;&lt;div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"&gt;&lt;div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;"&gt;SSHD&lt;br /&gt;Process&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="525" y="227" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle"&gt;SSHD&amp;#8230;&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;path d="M 195 204.89 L 195 169.8 L 213.58 165.22 L 229.15 169.93 L 229.15 205.42 L 210.83 209.74 Z" fill="#abb4c5" stroke="none" pointer-events="all"/&gt;&lt;path d="M 195 169.8 L 213.58 165.22 L 229.15 169.93 L 210.83 174.39 Z" fill="#b8bfd1" stroke="none" pointer-events="all"/&gt;&lt;path d="M 210.83 174.39 L 229.15 169.93 L 229.15 205.42 L 210.83 209.74 Z" fill="#5b6369" stroke="none" pointer-events="all"/&gt;&lt;path d="M 211.02 193.68 C 207.61 192.37 204.34 190.7 201.28 188.71 C 199.81 187.69 198.56 186.38 197.62 184.86 C 196.97 183.67 196.63 182.34 196.65 180.98 C 196.59 179.53 196.62 178.07 196.73 176.61 C 196.8 175.31 196.94 174.02 197.15 172.74 C 197.34 172.03 197.63 171.35 198.01 170.72 C 198.8 170.18 199.71 169.74 200.68 169.41 C 202.34 168.85 204.02 168.36 205.73 167.97 C 207.51 167.49 209.32 167.14 211.15 166.92 C 212.39 166.77 213.64 166.72 214.89 166.79 C 216.92 166.94 218.92 167.38 220.83 168.1 L 227.03 170.38 C 227.58 171.28 227.93 172.29 228.08 173.34 C 228.25 174.75 228.35 176.16 228.37 177.58 C 228.47 180.71 228.38 183.84 228.1 186.95 C 228.07 188.33 227.6 189.66 226.77 190.75 C 225.56 192.09 223.92 192.97 222.14 193.24 C 220.71 193.46 219.27 193.57 217.82 193.55 Z" fill-opacity="0.4" fill="#ffffff" stroke="none" pointer-events="all"/&gt;&lt;rect x="195" y="165.22" width="0" height="0" fill="none" stroke="none" pointer-events="all"/&gt;&lt;path d="M 196.83 175.17 L 196.83 174.25 L 208.74 177.92 L 208.74 178.84 Z M 198.79 202.93 L 198.79 202.01 L 206.12 204.11 L 206.12 205.02 Z" fill="#52595e" stroke="none" pointer-events="all"/&gt;&lt;path d="M 195 204.89 L 195 169.8 L 213.58 165.22 L 229.15 169.93 L 229.15 205.42 L 210.83 209.74 Z" fill="none" stroke="#000000" stroke-width="0.31" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;rect x="195" y="165.22" width="0" height="0" fill="none" stroke="none" pointer-events="all"/&gt;&lt;ellipse cx="224.81" cy="202.43" rx="9.839712136081127" ry="9.846174392076122" fill="#008cce" stroke="none" pointer-events="all"/&gt;&lt;path d="M 218.09 195.38 C 218.19 195.65 218.32 195.91 218.49 196.15 C 218.76 196.18 219.02 196.26 219.26 196.4 C 219.2 196.67 219.31 196.96 219.55 197.11 C 219.74 197.22 219.86 197.4 219.89 197.61 C 219.9 197.72 219.95 197.82 220.03 197.88 C 220.11 197.95 220.21 197.99 220.32 197.98 C 220.44 197.96 220.56 197.95 220.69 197.95 C 220.95 197.92 221.22 197.97 221.45 198.11 L 222.28 198.71 C 222.41 198.8 222.51 198.94 222.55 199.1 C 222.59 199.25 222.59 199.41 222.55 199.56 C 222.48 199.58 222.41 199.57 222.35 199.54 C 222.3 199.41 222.2 199.3 222.08 199.24 C 221.92 199.18 221.74 199.18 221.58 199.24 C 221.37 199.36 221.12 199.4 220.88 199.36 C 220.68 199.37 220.5 199.47 220.38 199.63 C 220.29 199.76 220.14 199.84 219.98 199.86 C 219.82 199.87 219.67 199.81 219.55 199.69 C 219.46 199.56 219.3 199.49 219.13 199.51 C 218.97 199.52 218.83 199.62 218.76 199.77 C 218.67 199.96 218.62 200.17 218.6 200.37 C 218.6 200.64 218.55 200.91 218.45 201.16 C 218.4 201.29 218.3 201.39 218.18 201.46 C 217.99 201.52 217.87 201.7 217.89 201.9 C 218.05 202.09 218.17 202.3 218.25 202.53 C 218.32 202.67 218.27 202.84 218.12 202.93 C 217.97 203.05 217.9 203.24 217.93 203.43 C 217.7 203.44 217.47 203.4 217.25 203.33 C 217.17 203.22 217.05 203.14 216.92 203.1 C 216.8 203.07 216.67 203.12 216.59 203.22 C 216.44 203.43 216.33 203.67 216.27 203.93 C 216.22 204.1 216.19 204.28 216.19 204.46 C 216.36 204.78 216.45 205.13 216.43 205.5 L 216.56 206.56 C 216.57 206.7 216.54 206.84 216.46 206.96 C 216.38 207.07 216.27 207.14 216.14 207.18 C 214.05 203.26 214.84 198.43 218.08 195.38 Z" fill="#00527f" stroke="none" pointer-events="all"/&gt;&lt;path d="M 230.12 194.25 C 230.08 194.4 229.99 194.53 229.86 194.61 C 229.74 194.66 229.61 194.69 229.49 194.69 L 228.76 194.78 C 228.44 194.75 228.12 194.78 227.82 194.89 C 227.85 194.99 227.81 195.11 227.72 195.17 C 227.64 195.23 227.52 195.24 227.43 195.18 C 227.24 195.19 227.06 195.27 226.93 195.41 C 226.84 195.52 226.71 195.59 226.56 195.61 C 226.36 195.65 226.19 195.78 226.1 195.97 C 226.01 196.13 225.85 196.16 225.74 196.05 C 225.67 195.9 225.62 195.74 225.61 195.58 C 225.48 195.27 225.24 195.01 224.94 194.85 C 224.64 194.72 224.35 194.54 224.1 194.32 C 223.94 194.2 223.84 194.01 223.84 193.81 C 223.83 193.69 223.84 193.57 223.88 193.45 C 223.78 193.25 223.57 193.1 223.34 193.08 C 223.17 193.04 223.03 192.91 222.97 192.75 C 225.5 192.28 228.12 192.82 230.26 194.25 Z" fill="#64aadc" stroke="none" pointer-events="all"/&gt;&lt;path d="M 234.25 205.1 C 234.22 204.79 234.1 204.5 233.91 204.26 C 233.71 204.24 233.51 204.34 233.41 204.52 C 233.25 204.76 233 204.92 232.72 204.96 C 232.52 204.74 232.56 204.44 232.81 204.26 C 232.97 204.02 233.01 203.71 232.92 203.43 C 232.9 203.31 232.85 203.2 232.79 203.1 C 232.6 202.91 232.46 202.69 232.39 202.43 C 232.21 202.55 232 202.53 231.84 202.39 C 231.68 202.25 231.59 202.01 231.61 201.77 C 231.62 201.48 231.52 201.21 231.32 201 C 231.34 200.54 231.2 200.1 230.92 199.73 C 230.65 199.46 230.28 199.29 229.89 199.27 C 229.56 199.27 229.29 199.08 229.28 198.83 C 229.27 198.59 229.53 198.38 229.86 198.38 C 230.36 198.43 230.86 198.42 231.35 198.34 C 231.83 198.28 232.29 198.13 232.72 197.91 L 233.18 197.27 C 234.61 199.62 235 202.45 234.25 205.1 Z" fill="#0a79b9" stroke="none" pointer-events="all"/&gt;&lt;path d="M 233.25 207.56 C 233.02 207.84 232.72 208.06 232.39 208.18 C 232.12 208.26 231.93 208.5 231.92 208.78 C 231.89 208.88 231.86 208.83 231.84 208.64 C 231.81 208.45 231.79 208.14 231.79 207.78 C 231.83 207.45 231.95 207.13 232.15 206.85 C 232.36 206.65 232.55 206.43 232.72 206.19 C 232.78 206.09 232.78 205.97 232.72 205.87 C 232.66 205.77 232.55 205.72 232.44 205.73 C 232.32 205.74 232.23 205.81 232.19 205.92 C 231.94 206.21 231.72 206.52 231.52 206.85 C 231.2 207.34 230.73 207.54 230.28 207.39 C 230.02 207.25 229.72 207.19 229.42 207.18 C 229.24 207.25 229.13 207.37 229.12 207.49 C 229.11 207.62 229.19 207.74 229.36 207.82 C 229.6 207.9 229.82 208.07 229.95 208.29 C 230.16 208.65 229.74 209.01 228.93 209.18 C 228.37 209.2 227.82 209.3 227.29 209.49 C 226.99 209.77 226.78 210.14 226.69 210.55 C 226.6 210.73 226.1 210.87 225.5 210.88 C 225.07 210.82 224.63 210.84 224.21 210.95 C 223.86 211.08 223.46 211.01 223.17 210.78 C 223.1 210.64 222.96 210.55 222.81 210.54 C 222.65 210.53 222.5 210.61 222.42 210.74 C 222.34 210.87 222.34 211.04 222.41 211.18 C 222.55 211.42 222.83 211.56 223.1 211.51 C 223.3 211.4 223.56 211.35 223.81 211.38 C 223.96 211.41 224.09 211.5 224.14 211.61 C 224.23 211.69 224.4 211.74 224.62 211.77 C 224.83 211.79 225.07 211.79 225.27 211.75 C 225.6 211.65 226.02 211.59 226.47 211.58 C 226.78 211.55 227.08 211.51 227.33 211.44 C 227.58 211.26 227.89 211.16 228.2 211.18 C 228.37 211.14 228.51 211.09 228.6 211.04 C 228.72 210.95 228.87 210.9 229.02 210.91 C 229.1 210.97 229.13 211.07 229.09 211.15 C 228.99 211.29 228.86 211.42 228.73 211.52 C 230.61 210.71 232.19 209.32 233.25 207.56 Z" fill="#266792" stroke="none" pointer-events="all"/&gt;&lt;ellipse cx="224.21" cy="198.24" rx="7.114164213280995" ry="4.923087196038061" fill-opacity="0.3" fill="#ffffff" stroke="none" pointer-events="all"/&gt;&lt;ellipse cx="220.96" cy="197.31" rx="2.0608439646712466" ry="1.5018034611318236" fill-opacity="0.4" fill="#ffffff" stroke="none" pointer-events="all"/&gt;&lt;rect x="195" y="165.22" width="0" height="0" fill="none" stroke="none" pointer-events="all"/&gt;&lt;ellipse cx="224.81" cy="202.43" rx="9.839712136081127" ry="9.846174392076122" fill="none" stroke="#000000" stroke-width="0.25" pointer-events="all"/&gt;&lt;rect x="195" y="165.22" width="0" height="0" fill="none" stroke="none" pointer-events="all"/&gt;&lt;path d="M 219.46 209.2 L 219.46 200.52 L 221.53 199.95 L 221.53 193.72 L 222.48 193.41 L 227.82 195.04 L 227.82 200.96 L 229.64 201.46 L 229.64 210.39 L 226 211.27 Z M 223.29 199.57 L 226.12 200.45 L 226.12 195.86 L 223.29 195.04 Z" fill="#a3acbd" stroke="none" pointer-events="all"/&gt;&lt;path d="M 229.64 201.46 L 226.06 202.34 L 219.46 200.52 L 222.98 199.51 Z" fill="#b2b9ca" stroke="none" pointer-events="all"/&gt;&lt;path d="M 229.64 210.39 L 226.06 211.27 L 226.06 202.34 L 229.64 201.46 Z" fill="#5b6369" stroke="none" pointer-events="all"/&gt;&lt;path d="M 226.06 210.71 L 219.46 208.7 L 219.46 207.88 L 226.06 209.83 L 229.64 208.95 L 229.64 209.83 Z" fill-opacity="0.7" fill="#0378b2" stroke="none" pointer-events="all"/&gt;&lt;rect x="195" y="165.22" width="0" height="0" fill="none" stroke="none" pointer-events="all"/&gt;&lt;path d="M 221.97 205.74 C 221.72 205.54 221.6 205.26 221.66 204.98 C 221.8 204.73 222.02 204.64 222.22 204.76 C 222.47 204.83 222.68 205.07 222.77 205.39 C 222.79 205.57 222.72 205.75 222.56 205.86 L 222.67 207.44 C 222.67 207.54 222.63 207.63 222.56 207.7 C 222.48 207.78 222.39 207.82 222.29 207.82 C 222.12 207.81 221.96 207.75 221.85 207.63 C 221.74 207.51 221.69 207.34 221.72 207.18 Z" fill="#3b393b" stroke="none" pointer-events="all"/&gt;&lt;path d="M 227.82 195.04 L 226.88 195.36 L 221.53 193.79 L 222.48 193.41 Z" fill="#d9dde6" stroke="none" pointer-events="all"/&gt;&lt;path d="M 226.06 203.52 L 219.46 201.65 L 219.46 200.82 L 226.06 202.64 L 229.64 201.76 L 229.64 202.64 Z" fill-opacity="0.7" fill="#0378b2" stroke="none" pointer-events="all"/&gt;&lt;path d="M 220.09 200.71 C 220.45 200.51 220.83 200.37 221.22 200.27 L 223.61 199.69 L 228.39 201.1 C 228.59 201.24 228.76 201.42 228.89 201.62 C 228.97 202.05 228.98 202.48 228.91 202.91 C 228.87 203.97 228.56 205 228.01 205.9 C 227.5 206.44 226.85 206.82 226.12 207 C 225.36 207.3 224.55 207.45 223.73 207.44 C 222.97 207.41 222.23 207.06 221.67 206.43 C 221.09 205.88 220.61 205.21 220.28 204.48 C 220.01 203.82 219.88 203.12 219.9 202.41 C 219.89 201.83 219.95 201.26 220.09 200.71 Z" fill-opacity="0.3" fill="#ffffff" stroke="none" pointer-events="all"/&gt;&lt;rect x="195" y="165.22" width="0" height="0" fill="none" stroke="none" pointer-events="all"/&gt;&lt;path d="M 221.53 193.79 L 226.88 195.36 L 226.88 201.9 L 226.12 201.71 L 226.12 195.86 L 222.41 194.73 L 222.41 200.52 L 221.53 200.27 Z" fill="#9ba5b6" stroke="none" pointer-events="all"/&gt;&lt;path d="M 227.82 195.04 L 227.82 201.65 L 226.88 201.9 L 226.88 195.36 Z" fill="#60686f" stroke="none" pointer-events="all"/&gt;&lt;path d="M 223.29 195.03 L 223.29 200.27 L 222.41 200.52 L 222.41 194.73 Z" fill="#5c6369" stroke="none" pointer-events="all"/&gt;&lt;path d="M 219.46 209.2 L 219.46 200.52 L 221.53 199.95 L 221.53 193.72 L 222.48 193.41 L 227.82 195.04 L 227.82 200.96 L 229.64 201.46 L 229.64 210.39 L 226 211.27 Z M 223.29 199.57 L 226.12 200.45 L 226.12 195.86 L 223.29 195.04 Z" fill="none" stroke="#000000" stroke-width="0.15" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 219px; margin-left: 215px;"&gt;&lt;div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"&gt;&lt;div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;"&gt;SOCKS5 Proxy&lt;br /&gt;by SSH&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="215" y="231" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle"&gt;SOCKS5&amp;#8230;&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;rect x="115" y="153.75" width="42.55" height="30" fill="none" stroke="none" pointer-events="all"/&gt;&lt;path d="M 115.06 172.69 L 123.57 172.69 L 123.57 170.76 L 115.06 170.76 Z M 115.06 166.74 L 125.27 166.74 L 125.27 164.81 L 115.06 164.81 Z M 115.06 160.78 L 126.97 160.78 L 126.97 158.85 L 115.06 158.85 Z M 146.63 170.76 L 133.31 170.76 L 134.53 165.89 L 147.85 165.89 Z M 133.02 163.96 L 130.84 172.69 L 148.14 172.69 L 150.32 163.96 Z M 150.07 181.52 C 150.03 181.69 149.88 181.82 149.7 181.82 L 126.09 181.82 C 125.93 181.82 125.83 181.73 125.79 181.68 C 125.74 181.62 125.68 181.51 125.71 181.35 L 130.08 160.74 L 154.48 160.74 Z M 131.47 155.68 L 155.08 155.68 C 155.24 155.68 155.33 155.77 155.38 155.82 C 155.42 155.88 155.49 155.99 155.45 156.15 L 154.89 158.81 L 130.49 158.81 L 131.09 155.99 C 131.13 155.81 131.29 155.68 131.47 155.68 Z M 156.87 154.61 C 156.43 154.06 155.78 153.75 155.08 153.75 L 131.47 153.75 C 130.38 153.75 129.43 154.52 129.2 155.59 L 123.82 180.96 C 123.68 181.64 123.85 182.35 124.29 182.89 C 124.73 183.44 125.38 183.75 126.09 183.75 L 149.7 183.75 C 150.78 183.75 151.73 182.98 151.96 181.92 L 157.34 156.55 C 157.49 155.86 157.32 155.15 156.87 154.61 Z" fill="#b0084d" stroke="none" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 191px; margin-left: 136px;"&gt;&lt;div data-drawio-colors="color: #232F3E; " style="box-sizing: border-box; font-size: 0px; text-align: center;"&gt;&lt;div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(35, 47, 62); line-height: 1.2; pointer-events: all; white-space: nowrap;"&gt;kubectl&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="136" y="203" fill="#232F3E" font-family="Helvetica" font-size="12px" text-anchor="middle"&gt;kubectl&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;rect x="115" y="240" width="42.55" height="30" fill="none" stroke="none" pointer-events="all"/&gt;&lt;path d="M 115.06 258.94 L 123.57 258.94 L 123.57 257.01 L 115.06 257.01 Z M 115.06 252.99 L 125.27 252.99 L 125.27 251.06 L 115.06 251.06 Z M 115.06 247.03 L 126.97 247.03 L 126.97 245.1 L 115.06 245.1 Z M 146.63 257.01 L 133.31 257.01 L 134.53 252.14 L 147.85 252.14 Z M 133.02 250.21 L 130.84 258.94 L 148.14 258.94 L 150.32 250.21 Z M 150.07 267.77 C 150.03 267.94 149.88 268.07 149.7 268.07 L 126.09 268.07 C 125.93 268.07 125.83 267.98 125.79 267.93 C 125.74 267.87 125.68 267.76 125.71 267.6 L 130.08 246.99 L 154.48 246.99 Z M 131.47 241.93 L 155.08 241.93 C 155.24 241.93 155.33 242.02 155.38 242.07 C 155.42 242.13 155.49 242.24 155.45 242.4 L 154.89 245.06 L 130.49 245.06 L 131.09 242.24 C 131.13 242.06 131.29 241.93 131.47 241.93 Z M 156.87 240.86 C 156.43 240.31 155.78 240 155.08 240 L 131.47 240 C 130.38 240 129.43 240.77 129.2 241.84 L 123.82 267.21 C 123.68 267.89 123.85 268.6 124.29 269.14 C 124.73 269.69 125.38 270 126.09 270 L 149.7 270 C 150.78 270 151.73 269.23 151.96 268.17 L 157.34 242.8 C 157.49 242.11 157.32 241.4 156.87 240.86 Z" fill="#b0084d" stroke="none" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 277px; margin-left: 136px;"&gt;&lt;div data-drawio-colors="color: #232F3E; " style="box-sizing: border-box; font-size: 0px; text-align: center;"&gt;&lt;div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(35, 47, 62); line-height: 1.2; pointer-events: all; white-space: nowrap;"&gt;aws cli&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="136" y="289" fill="#232F3E" font-family="Helvetica" font-size="12px" text-anchor="middle"&gt;aws cli&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;path d="M 545 183.75 L 688.63 183.75" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/&gt;&lt;path d="M 693.88 183.75 L 686.88 187.25 L 688.63 183.75 L 686.88 180.25 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;path d="M 480.7 72.67 L 489.28 72.67 L 489.28 67.89 C 489.28 66.64 488.46 64.69 486.66 64.69 L 483.25 64.69 C 482 64.69 480.7 65.98 480.7 67.89 Z M 475 90 L 475 72.67 L 477.83 72.67 L 477.83 65.36 C 477.83 62.48 479.81 60 482.05 60 L 487.91 60 C 490.18 60 492.13 62.43 492.13 65.49 L 492.13 72.67 L 495 72.67 L 495 90 Z" fill="#d9a741" stroke="none" pointer-events="all"/&gt;&lt;path d="M 489.28 67.89 C 489.28 66.64 488.46 64.69 486.66 64.69 L 483.25 64.69 C 482 64.69 480.7 65.99 480.7 67.89 L 480.7 66.87 C 480.7 65.2 481.77 63.63 483.26 63.63 L 486.75 63.63 C 488.02 63.63 489.28 65.03 489.28 66.82 Z M 475 90 L 475 88.98 L 495 88.98 L 495 90 Z" fill-opacity="0.3" fill="#000000" stroke="none" pointer-events="all"/&gt;&lt;path d="M 47.26 100.88 C 45.95 100.88 45 99.85 45 98.69 L 45 61.03 C 45 59.99 45.83 58.88 47.12 58.88 L 72.84 58.88 C 73.96 58.88 75 59.79 75 61.08 L 75 98.94 C 75 99.94 74.01 100.88 72.82 100.88 Z" fill="#7d7c7c" stroke="none" pointer-events="all"/&gt;&lt;path d="M 75 96.78 L 75 98.94 C 75 99.94 74.01 100.88 72.82 100.88 L 47.26 100.88 C 45.95 100.88 45 99.84 45 98.69 L 45 97.04 C 45 98.19 46.1 99.01 47.09 99.01 L 72.78 99.01 C 73.96 99.01 75 98.07 75 96.78 Z" fill-opacity="0.3" fill="#000000" stroke="none" pointer-events="all"/&gt;&lt;rect x="45" y="58.88" width="0" height="0" fill="none" stroke="rgb(0, 0, 0)" pointer-events="all"/&gt;&lt;path d="M 54.63 66.43 L 48.56 66.43 L 48.56 63.97 L 54.63 63.97 Z M 62.98 66.43 L 56.91 66.43 L 56.91 63.97 L 62.98 63.97 Z M 71.34 66.43 L 65.26 66.43 L 65.26 63.97 L 71.34 63.97 Z M 54.63 70.73 L 48.56 70.73 L 48.56 68.27 L 54.63 68.27 Z M 62.98 70.73 L 56.91 70.73 L 56.91 68.27 L 62.98 68.27 Z M 71.34 70.73 L 65.26 70.73 L 65.26 68.27 L 71.34 68.27 Z M 54.63 75.04 L 48.56 75.04 L 48.56 72.58 L 54.63 72.58 Z M 62.98 75.04 L 56.91 75.04 L 56.91 72.58 L 62.98 72.58 Z M 71.34 75.04 L 65.26 75.04 L 65.26 72.58 L 71.34 72.58 Z M 54.63 79.34 L 48.56 79.34 L 48.56 76.88 L 54.63 76.88 Z M 62.98 79.34 L 56.91 79.34 L 56.91 76.88 L 62.98 76.88 Z M 71.34 79.34 L 65.26 79.34 L 65.26 76.88 L 71.34 76.88 Z M 54.63 83.65 L 48.56 83.65 L 48.56 81.19 L 54.63 81.19 Z M 62.98 83.65 L 56.91 83.65 L 56.91 81.19 L 62.98 81.19 Z M 71.34 83.65 L 65.26 83.65 L 65.26 81.19 L 71.34 81.19 Z M 54.63 87.95 L 48.56 87.95 L 48.56 85.49 L 54.63 85.49 Z M 62.98 87.95 L 56.91 87.95 L 56.91 85.49 L 62.98 85.49 Z M 71.34 87.95 L 65.26 87.95 L 65.26 85.49 L 71.34 85.49 Z" fill="#ffffff" stroke="none" pointer-events="all"/&gt;&lt;path d="M 745.96 76 C 740.09 76 735.55 71.43 735.55 67.19 L 735.55 65.43 C 735.09 60.07 739.29 55.87 742.21 55.04 C 742.04 51.25 743.66 46.93 747.09 44.04 C 752.05 40 757.87 40.62 761.39 42.53 C 763.87 43.81 766.1 46.07 767.39 49.06 C 770.04 47.23 772.87 47.78 774.49 48.73 C 776.67 50.01 777.84 52.28 777.87 54.5 C 780.12 54.75 782.79 55.78 785.11 58.85 C 786.32 60.5 787.06 63.42 787.09 65.17 L 787.09 66.98 C 787.09 71.56 782.32 76 776.7 76 Z" fill="#f58536" stroke="none" pointer-events="all"/&gt;&lt;path d="M 787.09 65.17 L 787.09 66.98 C 787.09 71.55 782.32 76 776.7 76 L 745.96 76 C 740.09 76 735.55 71.42 735.55 67.19 L 735.55 65.43 C 735.55 69.28 739.85 74.11 745.88 74.11 L 776.24 74.11 C 782.68 74.11 787.09 69.52 787.09 65.17 Z" fill-opacity="0.3" fill="#000000" stroke="none" pointer-events="all"/&gt;&lt;rect x="735.09" y="40" width="0" height="0" fill="none" stroke="rgb(0, 0, 0)" pointer-events="all"/&gt;&lt;path d="M 748.15 64.72 L 750.99 64.72 L 749.61 58.59 Z M 750.92 56.94 L 754.14 69.34 L 752.09 69.34 L 751.36 66.32 L 747.85 66.32 L 747.04 69.34 L 744.99 69.34 L 748.43 56.94 Z M 766.29 56.94 L 763.88 69.34 L 761.6 69.34 L 760.03 59.73 L 758.47 69.34 L 756.26 69.34 L 753.92 56.94 L 755.82 56.94 L 757.47 66.81 L 759.04 56.94 L 761.09 56.94 L 762.74 66.88 L 764.32 56.94 Z M 774.58 60.11 L 772.67 60.11 C 772.71 59.17 772.47 58.55 771.87 58.25 C 771.5 58.07 770.71 57.92 770.1 58.29 C 769.77 58.5 769.36 58.89 769.37 59.92 C 769.39 61.23 770.33 61.72 771.25 62.09 C 772.37 62.45 773.36 62.76 774.09 63.47 C 775.22 64.55 775.11 66.27 774.79 67.31 C 774.35 68.59 773.35 69.21 772 69.42 C 770.8 69.56 769.51 69.53 768.56 68.81 C 767.49 67.95 767.29 66.68 767.32 65.41 L 769.23 65.41 C 769.2 66.25 769.31 66.91 769.65 67.33 C 770.17 67.99 771.07 68.09 771.85 67.88 C 772.8 67.58 773.05 66.69 773.02 66.12 C 773.04 65.06 772.51 64.38 771.57 64.06 C 770.04 63.52 768.88 63.28 768.05 62.3 C 767.63 61.82 767.22 60.83 767.46 59.28 C 767.67 57.98 768.53 57.22 769.31 56.9 C 770.4 56.39 772.17 56.46 773.12 56.94 C 774.09 57.39 774.65 58.38 774.58 60.11 Z" fill="#ffffff" stroke="none" pointer-events="all"/&gt;&lt;path d="M 695 163.71 L 735.09 163.71 L 735.09 203.8 L 695 203.8 Z" fill="url(#mx-gradient-f78e04-1-d05c17-1-s-0)" stroke="none" pointer-events="all"/&gt;&lt;path d="M 718.61 179.1 L 714.77 183.55 L 718.95 188.43 L 717.39 188.43 L 713.64 184.05 L 713.64 188.55 L 712.52 188.55 L 712.52 178.99 L 713.64 178.99 L 713.64 182.92 L 717.11 179.1 Z M 728.26 188.12 L 723.76 185.42 L 723.76 178.99 C 723.76 178.79 723.65 178.6 723.48 178.5 L 717.01 174.73 L 717.01 169.29 L 728.26 175.94 Z M 729.11 175.13 L 716.74 167.82 C 716.56 167.72 716.35 167.72 716.17 167.82 C 716 167.92 715.89 168.11 715.89 168.31 L 715.89 175.05 C 715.89 175.25 715.99 175.44 716.17 175.54 L 722.63 179.31 L 722.63 185.74 C 722.63 185.93 722.74 186.12 722.91 186.22 L 728.53 189.59 C 728.62 189.64 728.72 189.67 728.82 189.67 C 728.91 189.67 729.01 189.65 729.1 189.6 C 729.27 189.5 729.38 189.31 729.38 189.11 L 729.38 175.62 C 729.38 175.42 729.28 175.23 729.11 175.13 Z M 715.01 198.59 L 701.83 191.58 L 701.83 175.94 L 713.08 169.29 L 713.08 174.74 L 707.15 178.51 C 706.99 178.62 706.89 178.8 706.89 178.99 L 706.89 188.55 C 706.89 188.76 707.01 188.95 707.2 189.05 L 714.76 192.98 C 714.92 193.07 715.11 193.07 715.28 192.98 L 722.61 189.19 L 727.13 191.9 Z M 728.55 191.44 L 722.92 188.06 C 722.76 187.96 722.55 187.96 722.38 188.05 L 715.02 191.85 L 708.02 188.21 L 708.02 179.3 L 713.94 175.53 C 714.1 175.42 714.2 175.25 714.2 175.05 L 714.2 168.31 C 714.2 168.11 714.09 167.92 713.92 167.82 C 713.74 167.72 713.53 167.72 713.35 167.82 L 700.98 175.13 C 700.81 175.23 700.71 175.42 700.71 175.62 L 700.71 191.92 C 700.71 192.13 700.82 192.32 701.01 192.42 L 714.75 199.73 C 714.84 199.77 714.93 199.79 715.02 199.79 C 715.11 199.79 715.21 199.77 715.29 199.72 L 728.53 192.41 C 728.71 192.31 728.82 192.13 728.82 191.93 C 728.82 191.73 728.72 191.54 728.55 191.44 Z" fill="#ffffff" stroke="none" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 184px; margin-left: 737px;"&gt;&lt;div data-drawio-colors="color: #232F3E; " style="box-sizing: border-box; font-size: 0px; text-align: left;"&gt;&lt;div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(35, 47, 62); line-height: 1.2; pointer-events: all; white-space: nowrap;"&gt;EKS&lt;br /&gt;Cluster&lt;br /&gt;Endpoint&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="737" y="187" fill="#232F3E" font-family="Helvetica" font-size="12px"&gt;EKS&amp;#8230;&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;path d="M 525.02 456 C 511.25 456 500 444.6 500 431.53 L 500 428.52 C 500 415.94 510.46 404 524.91 404 C 539.69 404 550 415.94 550 428.72 L 550 431.14 C 550 444.73 538.98 456 525.02 456 Z" fill="#f58534" stroke="none" pointer-events="all"/&gt;&lt;path d="M 500 428.52 C 500 443.8 512.18 453.86 524.95 453.86 C 539.44 453.86 550 442.07 550 428.72 L 550 431.14 C 550 444.76 538.98 456 525.02 456 C 511.14 456 500 444.59 500 431.53 Z" fill-opacity="0.3" fill="#000000" stroke="none" pointer-events="all"/&gt;&lt;rect x="500" y="404" width="0" height="0" fill="none" stroke="rgb(0, 0, 0)" pointer-events="all"/&gt;&lt;path d="M 513.25 429.26 C 515.12 436.4 519.07 442.04 525 444.99 C 528.79 443.32 531.86 439.82 533.52 437.03 C 536.51 432.03 537.91 426.02 537.97 419.67 C 536.39 419.48 534.76 418.95 533.83 418.06 C 533 417.31 532.14 415.84 531.81 414 C 529.38 413.22 526.9 412.74 524.75 412.78 C 522.25 412.87 520.14 413.42 518.33 414 C 518.08 415.61 517.25 417.28 516.28 418.08 C 515.03 419.12 513.52 419.53 512.01 419.74 C 512.02 421.85 512.25 424.14 512.62 426.3 L 524.75 426.3 L 524.75 421.46 L 532.48 427.86 L 524.75 434.18 L 524.75 429.26 Z M 504.85 429.26 L 504.85 426.3 L 510.15 426.3 C 509.67 423.41 509.5 420.39 509.54 417.44 C 511.74 417.55 513.73 417.13 514.61 416.23 C 515.54 415.24 515.82 413.79 515.95 412.36 C 518.5 411.29 521.71 410.43 524.64 410.4 C 527.82 410.32 530.93 411.13 534.05 412.19 C 534.21 414.49 534.9 415.95 535.94 416.49 C 537.29 417.12 538.95 417.22 540.45 417.27 C 540.45 425.99 539.22 431.56 536 437.45 C 533.77 441.47 530.08 445.42 524.99 447.55 C 520.99 446.01 517.2 442.74 514.59 438.57 C 512.83 435.81 511.5 432.53 510.75 429.26 Z" fill="#ffffff" stroke="none" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 463px; margin-left: 525px;"&gt;&lt;div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"&gt;&lt;div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;"&gt;AWS Service Endpoint&lt;br /&gt;https://eks.us-west-2.amazonaws.com&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="525" y="475" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle"&gt;AWS Serv&amp;#8230;&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;path d="M 157.49 255 L 230 255 L 230 430 L 493.63 430" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/&gt;&lt;path d="M 498.88 430 L 491.88 433.5 L 493.63 430 L 491.88 426.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;path d="M 157.49 174.14 L 188.83 182.1" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/&gt;&lt;path d="M 193.92 183.39 L 186.27 185.06 L 188.83 182.1 L 187.99 178.28 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;path d="M 290 410 C 266 410 260 430 279.2 434 C 260 442.8 281.6 462 297.2 454 C 308 470 344 470 356 454 C 380 454 380 438 365 430 C 380 414 356 398 335 406 C 320 394 296 394 290 410 Z" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 430px; margin-left: 261px;"&gt;&lt;div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"&gt;&lt;div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"&gt;Internet&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="320" y="434" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle"&gt;Internet&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;rect x="0" y="390" width="210" height="110" fill="none" stroke="none" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 208px; height: 1px; padding-top: 445px; margin-left: 2px;"&gt;&lt;div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"&gt;&lt;div style="display: inline-block; font-size: 14px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"&gt;Note: kubectl calls aws-cli for authentication. So make sure that aws-cli uses the right profile and assumes the right role, if applicable.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="2" y="449" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="14px"&gt;Note: kubectl calls aws-cli fo&amp;#8230;&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;path d="M 115.06 168.81 L 50 169 L 52.43 383.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" stroke-dasharray="8 8" pointer-events="stroke"/&gt;&lt;path d="M 52.49 388.88 L 48.91 381.92 L 52.43 383.63 L 55.91 381.84 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;path d="M 356.25 193.75 L 359.36 333.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" stroke-dasharray="8 8" pointer-events="stroke"/&gt;&lt;path d="M 359.48 338.88 L 355.82 331.96 L 359.36 333.63 L 362.82 331.81 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;rect x="286" y="340" width="294" height="40" fill="none" stroke="none" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 292px; height: 1px; padding-top: 360px; margin-left: 288px;"&gt;&lt;div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"&gt;&lt;div style="display: inline-block; font-size: 14px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"&gt;The SSH Tunnel is established on top of a proxy command using SSM session manager or EC2 Instance Connect with EIC Endpoint&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="288" y="364" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="14px"&gt;The SSH Tunnel is established on top of a&amp;#8230;&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;path d="M 215.45 165.22 L 217.38 65.25" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/&gt;&lt;path d="M 217.48 60 L 220.84 67.06 L 217.38 65.25 L 213.84 66.93 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/&gt;&lt;rect x="150" y="8.88" width="270" height="50" fill="none" stroke="none" pointer-events="all"/&gt;&lt;g transform="translate(-0.5 -0.5)"&gt;&lt;switch&gt;&lt;foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"&gt;&lt;div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 268px; height: 1px; padding-top: 34px; margin-left: 152px;"&gt;&lt;div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;"&gt;&lt;div style="display: inline-block; font-size: 14px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"&gt;Tell kubectl to use SOCKS5 proxy by the HTTPS_PROXY environment variable or by the proxy-url attribute in .kube/config&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/foreignObject&gt;&lt;text x="152" y="38" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="14px"&gt;Tell kubectl to use SOCKS5 proxy by th&amp;#8230;&lt;/text&gt;&lt;/switch&gt;&lt;/g&gt;&lt;/g&gt;&lt;switch&gt;&lt;g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/&gt;&lt;a transform="translate(0,-5)" xlink:href="https://www.drawio.com/doc/faq/svg-export-text-problems" target="_blank" rel="noopener"&gt;&lt;text text-anchor="middle" font-size="10px" x="50%" y="100%"&gt;Text is not SVG &amp;#8211; cannot display&lt;/text&gt;&lt;/a&gt;&lt;/switch&gt;&lt;/svg&gt;&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;To put this in practice, I first created a VPC stack with a bastion host using terraform template from my &lt;a href="https://github.com/digihunch/vpc-base/tree/main"&gt;vpc-base&lt;/a&gt; project. The terraform output will give the next set of commands to run to create a private cluster, using a manifest rendered from the file &lt;a href="https://github.com/digihunch/vpc-base/blob/main/template/eksctl.tpl"&gt;private-cluster.yaml.tmpl&lt;/a&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# cd aws_vpc&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# terraform init&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# terraform plan&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# terraform apply&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ... run the given command ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# envsubst &amp;lt; private-cluster.yaml.tmpl | tee | eksctl create cluster -f -&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Run this from a remote host without access to cluster endpoint.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Run terraform apply and terraform output contains the variables needed for the next steps&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# the command below may take 15 minutes to create a private cluster&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;eksctl create cluster -f private-cluster.yaml &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;aws eks update-kubeconfig --name private-cluster&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;At this point, the kubeconfig file has been updated, but kubectl (from Internet or on-prem) is unable to connect to cluster endpoint (on private network). In order to &lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;BASTION_SECURITY_GROUP_ID&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;terraform output -raw bastion_sg_id&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CLUSTER_SECURITY_GROUP_ID&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;aws eks describe-cluster --name private-cluster --query &lt;span style="color:#e6db74"&gt;&amp;#34;cluster.resourcesVpcConfig.clusterSecurityGroupId&amp;#34;&lt;/span&gt; --output text&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# In Cluster Endpoint&amp;#39;s security group, open up port 443 to Bastion host&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;aws ec2 authorize-security-group-ingress --group-id $CLUSTER_SECURITY_GROUP_ID --source-group $BASTION_SECURITY_GROUP_ID --protocol tcp --port &lt;span style="color:#ae81ff"&gt;443&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Test with connecting to Bastion host with ssh i-0750643179667a5b6, assuming .ssh/config file is configured as above. From the bastion host, you can test:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# curl -k https://EC5405EE1846F19F9F61ED28FB12A6A9.sk1.us-west-2.eks.amazonaws.com/api &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# if you get an HTTP response, even an error code 403, the bastion host has TCP connectivity to cluster endpoint&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# then we can start an SSH session as a SOCKS5 proxy on the remote host&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh -D &lt;span style="color:#ae81ff"&gt;1080&lt;/span&gt; -q -N i-0750643179667a5b6&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# add &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp; to push it to background, or use ctrl+z after running the command&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# to validate that the SOCKS5 proxy is working, you can run the same curl command with a proxy parameter:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# curl -k https://EC5405EE1846F19F9F61ED28FB12A6A9.sk1.us-west-2.eks.amazonaws.com/api --proxy socks5://localhost:1080&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# you can instruct kubectl to use the SOCKS5 proxy with the following environment variable&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;export HTTPS_PROXY&lt;span style="color:#f92672"&gt;=&lt;/span&gt;socks5://localhost:1080&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl get node&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# alternatively, add &amp;#34;proxy-url: socks5://localhost:1080&amp;#34; below server attribute in ~/.kube/config file.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;There are some pitfalls to watch for. On the remote host both ssh command and kubectl command implicitly uses AWS CLI. Therefore, make sure the profile and IAM role are correctly configured. For example, if SSM agent requires one IAM role, and kubectl is created with another IAM role, then make sure AWS CLI &lt;a href="https://repost.aws/knowledge-center/iam-assume-role-cli"&gt;assumes the correct IAM role&lt;/a&gt; using environment variables, and use &amp;#8220;aws sts get-caller-identity&amp;#8221; to validate the IAM identity being used.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;What about AKS in Azure&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;I touched on this in my &lt;a href="https://static.digihunch.com/2021/10/notes-on-azure/"&gt;Azure notes&lt;/a&gt; in 2021 and did a research again. Unfortunately, options are still fairly limited. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The first option is to use a managed service called &amp;#8220;Azure Bastion&amp;#8221;, which requires public IP and a dedicated subnet with the exact name of AzureBastionSubnet, as well as some &lt;a href="https://learn.microsoft.com/en-us/azure/bastion/configuration-settings#subnet"&gt;additional requirement&lt;/a&gt;. I&amp;#8217;m not impressed with these requirement because it is meant to be a managed service. The other option, is essentially to DIY a JumpBox. The idea is the same: put the jumpbox in a public subnet, which is routable to private subnets. When you need to connect to private VMs, get to the jumpbox first.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Apart from having to put the bastion VM on a public subnet, the pattern that we discussed above involving SOCKS5 proxy still works. Exposing a bastion host isn&amp;#8217;t ideal but it still reduces attack surface significantly, comparing to exposing the cluster endpoints of all Kubernetes API servers.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Summary&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Many immature Kubernetes configurations exposes private endpoint publicly. Having cluster endpoint in private subnet greatly improves security posture. In my opinion, there are very few situations where cluster endpoint must exposed publicly. Having private endpoint should be mandatory for all Kubernetes cluster. In the next &lt;a href="https://static.digihunch.com/2023/06/kubernetes-paas-and-red-hat-openshift/"&gt;post&lt;/a&gt;, I also cover how to create a ROSA cluster with private endpoint.&lt;/p&gt;&#10;&lt;nav class="wp-post-navigation" aria-label="Post navigation"&gt;&#10;&lt;a rel="prev" href="https://static.digihunch.com/2023/05/kubernetes-with-multiple-cpu-architectures/"&gt;&lt;span class="wp-post-navigation-label"&gt;Previous Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;Kubernetes with Multiple CPU Architectures 2 of 2 – Node and Workload&lt;/strong&gt;&lt;/a&gt;&#10;&lt;a rel="next" href="https://static.digihunch.com/2023/06/kubernetes-paas-and-red-hat-openshift/"&gt;&lt;span class="wp-post-navigation-label"&gt;Next Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;Kubernetes Platform as a Service and Red Hat OpenShift&lt;/strong&gt;&lt;/a&gt;&#10;&lt;/nav&gt;&#10;</description></item><item><title>Key mapping for external PC keyboard on Mac</title><link>https://static.digihunch.com/2022/11/key-mapping-on-external-pc-keyboard-on-macbook/</link><pubDate>Wed, 23 Nov 2022 10:38:00 -0400</pubDate><guid>https://static.digihunch.com/2022/11/key-mapping-on-external-pc-keyboard-on-macbook/</guid><description>&lt;img src="https://static.digihunch.com/wp-content/uploads/2025/04/key-mapping-image.webp" alt="Featured image of post Key mapping for external PC keyboard on Mac" /&gt;&lt;p class="wp-block-paragraph"&gt;This post is to document my steps to use external Windows keyboard on Mac with custom key mapping.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading" id="h-background"&gt;Background&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Apple&amp;#8217;s magic keyboard does not support multi-device so I have to repurpose my Logitech K810 keyboard with MacBook. Logitech K810 is and &lt;a href="https://www.pcmag.com/reviews/logitech-k810-bluetooth-illuminated-keyboard"&gt;old model&lt;/a&gt; with Windows key layout even though it also supports MacOS.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The bottom row has the same number of keys as magic keyboard but with different keys. For example, magic keyboard has cmd key (aka GUI keys) besides space bar but K810 has Alt. Magic keyboard has Option key next to cmd key and K810 has Windows key on the left and Ctrl key on the right. The position of left Ctrl and FN is different on magic keyboard too. Since I rely on &lt;a href="https://static.digihunch.com/2018/02/linux-tips-and-tricks-in-shell/"&gt;shortcuts&lt;/a&gt; on Magic keyboard, I therefore decide configure key mapping on the Windows key, Alt keys and right ctrl key to match magic keyboard. &lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&#10;&lt;figure class="aligncenter size-full"&gt;&lt;img loading="lazy" decoding="async" width="1024" height="450" src="https://static.digihunch.com/wp-content/uploads/2025/04/K810.webp" alt="" class="wp-image-12872" srcset="https://static.digihunch.com/wp-content/uploads/2025/04/K810.webp 1024w, https://static.digihunch.com/wp-content/uploads/2025/04/K810-300x132.webp 300w, https://static.digihunch.com/wp-content/uploads/2025/04/K810-768x338.webp 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /&gt;&lt;figcaption class="wp-element-caption"&gt;K810 Layout&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In a nutshell, I need to remap some keys on an external Windows keyboard connected to MacOS (Monterey).&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;What is not working&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In MacOS, you could modify keys from keyboard preference as below. However, in my case I need to keep left ctrl key and overwrite the right ctrl key. MacOS does not distinguish them unfortunately. The native method is not flexible so I did not bother.&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&#10;&lt;figure class="aligncenter size-full"&gt;&lt;img loading="lazy" decoding="async" width="300" height="206" src="https://static.digihunch.com/wp-content/uploads/2025/04/macos-keyboard-pref.webp" alt="" class="wp-image-12871"/&gt;&lt;figcaption class="wp-element-caption"&gt;Use MacOS keyboard preference to modify keys&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In the mean time, I hate to install a third-party application (such as &lt;a href="https://karabiner-elements.pqrs.org/"&gt;Karabiner-Elements&lt;/a&gt;) just for the purpose of key mapping. Even though I appreciate the efforts by the community, I just don&amp;#8217;t find it a neat solution for the additional dependency.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Challenges with key mapping&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;I then came across a developer&amp;#8217;s &lt;a href="https://rakhesh.com/mac/using-hidutil-to-map-macos-keyboard-keys/"&gt;post&lt;/a&gt; on using a utility called hidutil to map keys. This is promising after I tested using this command to swap key a/A and b/B. &lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;hidutil property --set &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;UserKeyMapping&amp;#34;:[{&amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;: 0x700000004,&amp;#34;HIDKeyboardModifierMappingDst&amp;#34;: 0x700000005},{&amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;: 0x700000005,&amp;#34;HIDKeyboardModifierMappingDst&amp;#34;: 0x700000004}]}&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;The command above takes effect immediately, on all keyboards. In my case I only need mapping on an external keyboard, not the built-in keyboard. Luckily hidutil has &amp;#8211;matching switch where I can specify ProductID and VendorID. I can find both of them from About This Mac -&amp;gt; System Report -&amp;gt; Hardware -&amp;gt; Bluetooth:&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&#10;&lt;figure class="aligncenter size-full"&gt;&lt;img loading="lazy" decoding="async" width="1024" height="654" src="https://static.digihunch.com/wp-content/uploads/2025/04/vendor-id-product-id.webp" alt="" class="wp-image-12870" srcset="https://static.digihunch.com/wp-content/uploads/2025/04/vendor-id-product-id.webp 1024w, https://static.digihunch.com/wp-content/uploads/2025/04/vendor-id-product-id-300x192.webp 300w, https://static.digihunch.com/wp-content/uploads/2025/04/vendor-id-product-id-768x491.webp 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /&gt;&lt;figcaption class="wp-element-caption"&gt;Vendor ID and Product ID&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;From &lt;a href="https://developer.apple.com/library/archive/technotes/tn2450/_index.html"&gt;this&lt;/a&gt; technical note, we can find out the usage IDs of the keys in the table at the bottom of the page. Then use the Usage ID as the last two bytes of the source and destination values of the mapping expression above. For example, 0x700000004 stands for key A/a, and 0x700000005 stands for key B/b. In my case, after going through this table, my key mapping is:&lt;/p&gt;&#10;&lt;figure class="wp-block-table"&gt;&lt;table class="has-very-light-gray-to-cyan-bluish-gray-gradient-background has-background"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Source Key on K810&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Source Code on K810&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Destination Key&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Destination Code&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Left Alt&lt;/td&gt;&lt;td&gt;0x7000000E2&lt;/td&gt;&lt;td&gt;Left GUI (cmd)&lt;/td&gt;&lt;td&gt;0x7000000E3&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Right Alt&lt;/td&gt;&lt;td&gt;0x7000000E6&lt;/td&gt;&lt;td&gt;Right GUI (cmd)&lt;/td&gt;&lt;td&gt;0x7000000E7&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Left Win&lt;/td&gt;&lt;td&gt;0x7000000E3&lt;/td&gt;&lt;td&gt;Left Alt (option)&lt;/td&gt;&lt;td&gt;0x7000000E2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Right Ctrl&lt;/td&gt;&lt;td&gt;0x7000000E4&lt;/td&gt;&lt;td&gt;Right Alt (option)&lt;/td&gt;&lt;td&gt;0x7000000E6&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fn+F12&lt;/td&gt;&lt;td&gt;0x700000045&lt;/td&gt;&lt;td&gt;Globe&lt;/td&gt;&lt;td&gt;0xFF00000003&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;There is even a &lt;a href="https://hidutil-generator.netlify.app/"&gt;website&lt;/a&gt; that helps you generate the usage ID. I put together a command, which is working, on the external keyboard.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;hidutil property --matching &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;ProductID&amp;#34;:0xB319,&amp;#34;VendorID&amp;#34;:0x046D}&amp;#39;&lt;/span&gt; --set &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;UserKeyMapping&amp;#34;:[{&amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;:0x7000000E2,&amp;#34;HIDKeyboardModifierMappingDst&amp;#34;:0x7000000E3},{&amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;:0x7000000E6,&amp;#34;HIDKeyboardModifierMappingDst&amp;#34;:0x7000000E7},&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;{&amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;:0x7000000E3,&amp;#34;HIDKeyboardModifierMappingDst&amp;#34;:0x7000000E2},&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;{&amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;:0x7000000E4,&amp;#34;HIDKeyboardModifierMappingDst&amp;#34;:0x7000000E6},&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;{&amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;:0x700000045,&amp;#34;HIDKeyboardModifierMappingDst&amp;#34;:0xFF00000003}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;]}&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;The other way to confirm that key mapping is working, is to look up key mapping with the following command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;hidutil property --matching &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;ProductID&amp;#34;:0xB319,&amp;#34;VendorID&amp;#34;:0x046D}&amp;#39;&lt;/span&gt; --get &lt;span style="color:#e6db74"&gt;&amp;#34;UserKeyMapping&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;To wipe out the mapping, we can use the following command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;hidutil property --matching &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;ProductID&amp;#34;:0xB319,&amp;#34;VendorID&amp;#34;:0x046D}&amp;#39;&lt;/span&gt; --set &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;UserKeyMapping&amp;#34;:[]}&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;Note that we have to use the same matching expression. However, I noticed that the command only works while the bluetooth keyboard is connected. When I reboot the MacBook the mapping goes away as well.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;The solution to key mapping&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Even though I could run this command after reboot using a plist file in ~/Library/LaunchAgents/, as the reference blog suggests, it will not work because I cannot guarantee the bluetooth keyboard is connected before MacOS executes it. Also, I might disconnect and re-connect the keyboard and I want to ensure that the system runs that command whenever the keyboard gets connected.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In the &lt;a href="https://github.com/pqrs-org/Karabiner-Elements"&gt;Karabiner Element&lt;/a&gt; community, some developers touched on &lt;a href="https://github.com/pqrs-org/Karabiner-Elements/issues/2196#issuecomment-621928482"&gt;this&lt;/a&gt;. Basically we can customize the plist file to tell MacOS when to fire the hidutil command, by using &amp;#8220;LaunchEvents&amp;#8221; key in the XML. There is an &lt;a href="https://github.com/pqrs-org/Karabiner-Elements/issues/2196#issuecomment-621928482"&gt;example&lt;/a&gt; for USB keyboard. For myself, I edit the plist file to be:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!DOCTYPE plist PUBLIC &amp;#34;-//Apple//DTD PLIST 1.0//EN&amp;#34; &amp;#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd&amp;#34;&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;plist&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;version=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;1.0&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Label&lt;span style="color:#f92672"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;local.hidutilKeyMapping&lt;span style="color:#f92672"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;LaunchEvents&lt;span style="color:#f92672"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.iokit.matching&lt;span style="color:#f92672"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.bluetooth.hostController&lt;span style="color:#f92672"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;IOProviderClass&lt;span style="color:#f92672"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;IOBluetoothHCIController&lt;span style="color:#f92672"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;idProduct&lt;span style="color:#f92672"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;B319&lt;span style="color:#f92672"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;idVendor&lt;span style="color:#f92672"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;integer&amp;gt;&lt;/span&gt;046D&lt;span style="color:#f92672"&gt;&amp;lt;/integer&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;IOMatchLaunchStream&lt;span style="color:#f92672"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span style="color:#f92672"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/usr/bin/hidutil&lt;span style="color:#f92672"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;property&lt;span style="color:#f92672"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;--matching&lt;span style="color:#f92672"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;{&amp;#34;ProductID&amp;#34;:0xB319,&amp;#34;VendorID&amp;#34;:0x046D}&lt;span style="color:#f92672"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;--set&lt;span style="color:#f92672"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;UserKeyMapping&amp;#34;: [&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; { &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;:0x7000000E2,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;HIDKeyboardModifierMappingDst&amp;#34;:0x7000000E3&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; { &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;:0x7000000E6,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;HIDKeyboardModifierMappingDst&amp;#34;:0x7000000E7&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; { &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;:0x7000000E3,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;HIDKeyboardModifierMappingDst&amp;#34;:0x7000000E2&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; { &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;:0x7000000E4,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;HIDKeyboardModifierMappingDst&amp;#34;:0x7000000E6&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;HIDKeyboardModifierMappingSrc&amp;#34;:0x700000045,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;HIDKeyboardModifierMappingDst&amp;#34;:0xFF00000003&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&lt;span style="color:#f92672"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;RunAtLoad&lt;span style="color:#f92672"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;Store the file above as ~/Library/LaunchAgents/com.local.hidutilKeyMapping.plist (create LaunchAgents directory if it doesn&amp;#8217;t exist) and then run the following to load it:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;launchctl load ~/Library/LaunchAgents/com.local.hidutilKeyMapping.plist&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;This works perfectly. The dict section (line 7 to line 23) of the file is based on &lt;a href="http://technologeeks.com/docs/launchd.pdf"&gt;this&lt;/a&gt; document as the GitHub &lt;a href="https://github.com/pqrs-org/Karabiner-Elements/issues/2196#issuecomment-621928482"&gt;comment&lt;/a&gt; suggests. The LaunchEvents section narrows down the type of events that will trigger the program with arguments. It triggers whenever a bluetooth device with specific ProductID and VendorID connects to Mac. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;We can still use hidutil command from the previous section to ensure that the custom key mapping remain in effect:&lt;/p&gt;&#10;&lt;figure class="wp-block-image size-large"&gt;&lt;img loading="lazy" decoding="async" width="1024" height="531" src="https://static.digihunch.com/wp-content/uploads/2025/04/hidutil-command-1024x531.webp" alt="" class="wp-image-12869" srcset="https://static.digihunch.com/wp-content/uploads/2025/04/hidutil-command-1024x531.webp 1024w, https://static.digihunch.com/wp-content/uploads/2025/04/hidutil-command-300x155.webp 300w, https://static.digihunch.com/wp-content/uploads/2025/04/hidutil-command-768x398.webp 768w, https://static.digihunch.com/wp-content/uploads/2025/04/hidutil-command-1536x796.webp 1536w, https://static.digihunch.com/wp-content/uploads/2025/04/hidutil-command-2048x1061.webp 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Note that the output above has usage values converted to decimal from hexadecimal, with destination on top and source at the bottom.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;I like this solution as it is light-weight, targeting a specific event, highly customizable and resolves an issue in a very specific circumstance.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Final words&lt;/h2&gt;&#10;&lt;div class="wp-block-image"&gt;&#10;&lt;figure class="aligncenter size-full"&gt;&lt;img loading="lazy" decoding="async" width="1024" height="91" src="https://static.digihunch.com/wp-content/uploads/2025/04/K380-layout.webp" alt="" class="wp-image-12868" srcset="https://static.digihunch.com/wp-content/uploads/2025/04/K380-layout.webp 1024w, https://static.digihunch.com/wp-content/uploads/2025/04/K380-layout-300x27.webp 300w, https://static.digihunch.com/wp-content/uploads/2025/04/K380-layout-768x68.webp 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /&gt;&lt;figcaption class="wp-element-caption"&gt;K380 layout&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;After all this customization effort, I came across the Logitech &lt;a href="https://www.logitech.com/en-us/products/keyboards/k380-multi-device.920-011135.html"&gt;K380&lt;/a&gt; model (current alternative to K810), whose layout is exactly the way I customized K810 to be. So I ordered the new K380 model for just $29.99 during the week of black Friday. Remembering that I had my K810 for $94.98 back in 2015, what&amp;#8217;s the point of all this&amp;#8230;.&lt;/p&gt;&#10;&lt;nav class="wp-post-navigation" aria-label="Post navigation"&gt;&#10;&lt;a rel="prev" href="https://static.digihunch.com/2022/11/aws-serverless-services-and-developer-tools/"&gt;&lt;span class="wp-post-navigation-label"&gt;Previous Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;AWS serverless services and developer tools&lt;/strong&gt;&lt;/a&gt;&#10;&lt;a rel="next" href="https://static.digihunch.com/2022/12/landing-zone-in-aws/"&gt;&lt;span class="wp-post-navigation-label"&gt;Next Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;Landing Zone in AWS – An Introduction&lt;/strong&gt;&lt;/a&gt;&#10;&lt;/nav&gt;&#10;</description></item><item><title>GraphQL and gRPC</title><link>https://static.digihunch.com/2022/10/graphql-and-grpc/</link><pubDate>Fri, 07 Oct 2022 14:49:00 -0400</pubDate><guid>https://static.digihunch.com/2022/10/graphql-and-grpc/</guid><description>&lt;img src="https://static.digihunch.com/wp-content/uploads/2025/04/feature-graphql-grpc.webp" alt="Featured image of post GraphQL and gRPC" /&gt;&lt;h2 class="wp-block-heading" id="h-big-picture"&gt;Big Picture&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;For inter-process communication at a high level, the two styles are asynchronous and synchronous styles:&lt;/p&gt;&#10;&lt;ul class="wp-block-list"&gt;&#10;&lt;li&gt;Asynchronous event-driven style: involving an event broker as a middle man. &lt;/li&gt;&#10;&lt;li&gt;Synchronous request-response style: including several families of technologies:&#10;&lt;ul class="wp-block-list"&gt;&#10;&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Remote_procedure_call"&gt;RPC&lt;/a&gt; (Remote Procedure Call):&#10;&lt;ul class="wp-block-list"&gt;&#10;&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture"&gt;CORBA&lt;/a&gt; (Common Object Request Broker Architecture)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Java_remote_method_invocation"&gt;Java RMI&lt;/a&gt; (Remote Method Invocation)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/SOAP"&gt;SOAP&lt;/a&gt; (Simple Object Access Protocol)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Representational_state_transfer"&gt;REST&lt;/a&gt; (Representational State Transfer)&lt;/li&gt;&#10;&lt;li&gt;gRPC&lt;/li&gt;&#10;&lt;li&gt;GraphQL&lt;/li&gt;&#10;&lt;li&gt;Apache Thrift&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p class="wp-block-paragraph"&gt;RPCs, built on top of TCP/UDP, are usually complex to implement. SOAP improved it and can operate on HTTP. Many large companies today still used SOAP for message exchange. However, it has a limitation with complex format and specifications for XML messaging, giving rise to REST. REST is not a standard, but rather a loosely defined architectural style. Its payload can be in any format (XML, JSON, etc) specified in the header. As long as the API conforms to certain guidelines (criteria outlined in &lt;a href="https://www.redhat.com/en/topics/api/what-is-a-rest-api"&gt;this&lt;/a&gt; article), we can consider the API RESTful. REST has been steadily replacing &lt;a href="https://www.redhat.com/en/topics/integration/whats-the-difference-between-soap-rest"&gt;SOAP&lt;/a&gt; in the past few years. In this post, I start with REST, then dive into gRPC and GraphQL.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;REST&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The de facto method of building microservices using REST architectural style is use HTTP protocol with JSON format payload. JSON format is human readable, but not optimized for machine-to-machine communication. So there is some room for &lt;a href="https://nilsmagnus.github.io/post/proto-json-sizes/"&gt;compression&lt;/a&gt;. To emulate a web request in REST, one can use curl, a common utility to emulate any HTTP client.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;REST has its shortcomings. For example, the interface between REST client and server is not strongly typed. You can choose to use &lt;a href="https://swagger.io/specification/"&gt;OpenAPI/Swagger&lt;/a&gt; specification to define types but it is still not tightly integrated. There is no enforcement on the format of the payload either. RESTful services are quite bulky, inefficient, and error-prone. gRPC and GraphQL emerged to address different challenges with REST. GraphQL operates on HTTP and we can view it as a &lt;a href="https://www.apollographql.com/blog/backend/layering-graphql-on-top-of-rest/"&gt;layer&lt;/a&gt; on top of REST in a broad sense. gRPC on the other hand, operates on HTTP /2, and thereby inherits many advantages from it.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;HTTP /2&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;HTTP/2 is the second major version of HTTP. It overcomes some issues with HTTP/1.1 on security, speed, etc. &lt;a href="https://www.digitalocean.com/community/tutorials/http-1-1-vs-http-2-what-s-the-difference"&gt;This&lt;/a&gt; post is a good rundown of the difference between HTTP /2 and HTTP /1.1, which account for many of the advantages of gRPC. A thorough discussion on the differences between the two HTTP versions is beyond what this post can cover. One of the important difference with HTTP/2, is that all communication between a client and server is performed over a single TCP connection that can carry any number of bidirectional flows of bytes. This makes gRPC a high-performance RPC framework. In HTTP/2, the key concepts to understand are: &lt;/p&gt;&#10;&lt;ul class="wp-block-list"&gt;&#10;&lt;li&gt;Stream: a bidirectional flow of bytes within an established connection. A stream may carry one or more messages;&lt;/li&gt;&#10;&lt;li&gt;Frame: the smallest unit of communication in HTTP/2. Each frame contains a frame header, which at a minimum identifies the stream to which the frame belongs. &lt;/li&gt;&#10;&lt;li&gt;Message: a complete sequence of frames that map to a logical HTTP message that consists of one or more frames. &lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The request message is always triggered by the client. During the interaction, the client and server break down the message into frames, interleave them, and then reassemble them on the other side. In this way HTTP /2 multiplex the messages, and enables the following communication patterns:&lt;/p&gt;&#10;&lt;ul class="wp-block-list"&gt;&#10;&lt;li&gt;Simple RPC: a single request and a single response in the communication;&lt;/li&gt;&#10;&lt;li&gt;Server Streaming RPC: a single request and message followed by multiple response messages;&lt;/li&gt;&#10;&lt;li&gt;Client streaming RPC: client sends multiple messages and the server replies with one response message;&lt;/li&gt;&#10;&lt;li&gt;Bi-directional RPC: client setups connection by sending header frames. Once connection is established, both client and server send messages simultaneously without waiting for the other to finish;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The streaming communication patterns fundamentally improves performance, enabling the duplex streaming capability for gRPC.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;gRPC&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;At the protocol level, gRPC has the following advantages over REST:&lt;/p&gt;&#10;&lt;ul class="wp-block-list"&gt;&#10;&lt;li&gt;well-defined service interface and schema&lt;/li&gt;&#10;&lt;li&gt;strongly typed data&lt;/li&gt;&#10;&lt;li&gt;duplex streaming (thanks to HTTP2)&lt;/li&gt;&#10;&lt;li&gt;built-in commodity features (e.g. authentication, encryption, resiliency, service discovery, etc)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p class="wp-block-paragraph"&gt;I previously touched on gRPC protocol in the context of &lt;a href="https://static.digihunch.com/2022/03/from-nginx-to-envoy-proxy/"&gt;Envoy&lt;/a&gt; and &lt;a href="https://static.digihunch.com/2022/06/etcd-the-key-value-store-for-kubernetes/"&gt;etcd&lt;/a&gt;. Envoy makes use of gRPC for its control plane, where it&amp;nbsp;&lt;a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/overview#config-overview"&gt;fetches configuration from management server(s)&lt;/a&gt;&amp;nbsp;and in filters, such as for&amp;nbsp;&lt;a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/rate_limit_filter#config-http-filters-rate-limit"&gt;rate limiting&lt;/a&gt;&amp;nbsp;or authorization checks. Etcd store implements gRPC protocol for client utility to communicate with. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Since the typical use case of gRPC is internal communication, I have not been able to find a playground online. To get a taste of gRPC client-server interaction, just play with &lt;a href="https://etcd.io/docs/v3.5/tutorials/"&gt;etcd&lt;/a&gt; on MacOS. gRPC is language neutral. To start developing, refer to the tutorial in different languages (e.g. &lt;a href="https://grpc.io/docs/languages/go/basics/"&gt;Golang&lt;/a&gt;, &lt;a href="https://grpc.io/docs/languages/python/quickstart/"&gt;Python&lt;/a&gt;). When developing a gRPC application, the first thing to do is define a service interface in IDL (interface definition language). gRPC uses &lt;a href="https://developers.google.com/protocol-buffers"&gt;protocol buffers&lt;/a&gt; as the IDL to define the service interface. Protocol buffers are a language-agnostic, platform-neutral, extensible mechanism to serializing structured data. Using that service interface definition, we can generate the server-side code known as a server skeleton. Also you can generate the client-side code, known as a client stub. The methods that you specify in the service interface definition can be remotely invoked by the client side as easily as making a local function invocation. &lt;/p&gt;&#10;&lt;figure class="wp-block-image size-large"&gt;&lt;img loading="lazy" decoding="async" width="1052" height="503" src="https://static.digihunch.com/wp-content/uploads/2022/08/image-5.png" alt="" class="wp-image-6910"/&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;gRPC also has some disadvantages. Currently, the ecosystem is still small. When we have a service interface, we have to maintain the interface across versions.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;GraphQL&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In most of the use cases of gRPC and GraphQL, GraphQL works for external-facing services/APIs while internal services backing the APIs are implemented using gRPC. Honeypot created good documentary for GraphQL available &lt;a href="https://www.youtube.com/watch?v=783ccP__No8"&gt;here&lt;/a&gt;. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;We use GraphQL for external facing services because it gives API client the ability to query. An SQL query allows client to filter requested data based on conditions, and define the interested columns in the data return. This capability is missing in the REST style guideline. One may choose to implement their RESTful service to support their client&amp;#8217;s query requirement, GraphQL standardizes this capability with typed data, thereby prevents unnecessary network round trips, over- and under-fetching of data.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The &lt;a href="https://graphql.org/learn/"&gt;official site&lt;/a&gt; is a good reference for learning. One needs to know concepts around queries and mutations, schema and types (scalars, variable, fragment, interfaces, unions) to understand how query works.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;&lt;a href="https://ivangoncharov.github.io/graphql-apis/"&gt;This&lt;/a&gt; page lists a number of publicly available services in GraphQL. For example, country information service is available &lt;a href="https://countries.trevorblades.com/"&gt;here&lt;/a&gt;. On the web page you can put in a query like this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;query&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;myCountry&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;countries&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;filter&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;{&lt;span style="color:#a6e22e"&gt;code&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;{&lt;span style="color:#66d9ef"&gt;in&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;CA&amp;#34;&lt;/span&gt;}}) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;code&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;capital&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;The query (with the name of myCountry) above asks to return entries with countries as type. Then it filters the result by the condition that the country code must include &amp;#8220;CA&amp;#8221;. The return should include code, name and capital columns. The web page looks like this:&lt;/p&gt;&#10;&lt;figure class="wp-block-image size-large"&gt;&lt;img loading="lazy" decoding="async" width="1073" height="376" src="https://static.digihunch.com/wp-content/uploads/2022/08/image-3.png" alt="" class="wp-image-6853"/&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;GraphQL is on HTTP, so I can emulate the call with curl and get the same result:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl --request POST https://countries.trevorblades.com/ &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;--header &lt;span style="color:#e6db74"&gt;&amp;#39;Content-Type: application/json&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;--data-raw &lt;span style="color:#e6db74"&gt;&amp;#39;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;query&amp;#34; : &amp;#34;query myCountry { countries (filter: { code: { in: \&amp;#34;CA\&amp;#34; } }) {code name capital} }&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;}&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;The return prints the same result:&lt;/p&gt;&#10;&lt;figure class="wp-block-image size-large"&gt;&lt;img loading="lazy" decoding="async" width="2442" height="318" src="https://static.digihunch.com/wp-content/uploads/2022/08/image-4.png" alt="" class="wp-image-6854"/&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The document has a &lt;a href="https://graphql.org/code/"&gt;page&lt;/a&gt; on library options for different languages, such as Graphene and Ariadne for Python. You can use Ariadne and Flask to build GraphQL API as &lt;a href="https://www.twilio.com/blog/graphql-api-python-flask-ariadne"&gt;this&lt;/a&gt; post suggests.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The example on &lt;a href="https://www.howtographql.com/basics/1-graphql-is-the-better-rest/"&gt;this&lt;/a&gt; page has a better example that highlights how GraphQL allows client to ask multiple questions at the same time, and how the response includes different pieces of answers. GraphQL backend often needs to connect to different downstream system. This requires a proxy service to connect to disparate data sources. Examples of such services with such capabilities include &lt;a href="https://www.apollographql.com/docs/"&gt;Apollo&lt;/a&gt;, AWS &lt;a href="https://aws.amazon.com/appsync/"&gt;AppSync&lt;/a&gt;, or &lt;a href="https://cloud.google.com/blog/products/api-management/interacting-with-apis-rest-and-graphql"&gt;ApiGee&lt;/a&gt;. &lt;/p&gt;&#10;&lt;h2 class="wp-block-heading"&gt;Summary&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;For API protocols, we often compare among REST, gRPC and GraphQL. REST is loosely defined and widely adopted for the past few years. The use case of REST diverged into two areas: external facing API and internal service-to-service communication. gRPC and GraphQL are relatively new and they each are good for one of the use cases.&lt;/p&gt;&#10;&lt;figure class="wp-block-image size-full"&gt;&lt;img loading="lazy" decoding="async" width="1051" height="696" src="https://static.digihunch.com/wp-content/uploads/2022/08/image-6.png" alt="" class="wp-image-6915"/&gt;&lt;figcaption class="wp-element-caption"&gt;Microservice pattern based on gRPC and GraphQL (source: &amp;#8220;gRPC up &amp;amp; running&amp;#8221; by Indrasiri &amp;amp; Kuruppu)&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The gRPC protocol on HTTP2 is often used in internal communication between &lt;a href="https://thenewstack.io/build-real-world-microservices-with-grpc/"&gt;microservices&lt;/a&gt;. GraphQL offers query capability and is therefore often used to face external client.&lt;/p&gt;&#10;&lt;nav class="wp-post-navigation" aria-label="Post navigation"&gt;&#10;&lt;a rel="prev" href="https://static.digihunch.com/2022/09/build-a-kubernetes-cluster/"&gt;&lt;span class="wp-post-navigation-label"&gt;Previous Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;Build and Manage Kubernetes Clusters&lt;/strong&gt;&lt;/a&gt;&#10;&lt;a rel="next" href="https://static.digihunch.com/2022/10/computing-from-paas-to-serverless/"&gt;&lt;span class="wp-post-navigation-label"&gt;Next Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;Computing services: from PaaS to Serverless&lt;/strong&gt;&lt;/a&gt;&#10;&lt;/nav&gt;&#10;</description></item><item><title>Kubernetes Admission Control</title><link>https://static.digihunch.com/2022/01/kubernetes-admission-control/</link><pubDate>Fri, 07 Jan 2022 22:21:00 -0400</pubDate><guid>https://static.digihunch.com/2022/01/kubernetes-admission-control/</guid><description>&lt;img src="https://static.digihunch.com/wp-content/uploads/2025/04/feature-k8s-admin-control.webp" alt="Featured image of post Kubernetes Admission Control" /&gt;&lt;p class="wp-block-paragraph"&gt;This post discusses admission control, and its implementation &amp;#8211; the OPA Gatekeeper. I also discuss Azure Policy as a different Gatekeeper implementation.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading" id="admission-webhooks"&gt;Admission Webhooks&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Admission controller intercepts requests to the Kubernetes API server after the request has been authenticated and authorized, and prior to persistence of the object into etcd store. There are many compiled-in &lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#what-does-each-admission-controller-do"&gt;controllers&lt;/a&gt;, which can be turned on and off on the node with the arguments of &lt;a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"&gt;kube-apiserver&lt;/a&gt; process. For example, the &lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#imagepolicywebhook"&gt;ImagePolicyWebhook&lt;/a&gt; can be enabled with value ImagePolicyWebhook added to the &amp;#8211;enable-admission-plugins switch. Its configuration can be provided via the &amp;#8211;admission-control-config-file &lt;a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/#options"&gt;switch&lt;/a&gt;. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In addition to the compiled-in admission plugins (which must be configured for kube-apiserver process on the node), admission plugins can be developed as extensions and run as webhooks configured at runtime. This allows users to configure webhooks via API access, dynamically without having to restart kube-apiserver process on the Node, which is usually hard to do with managed Kubernetes platforms. They are therefore called Dynamic Admission Control.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;You can define two types of admission webhooks in dynamic admission control: validating admission webhook, and mutating admission webhook. Their interaction with API server can be illustrated in the diagram below:&lt;/p&gt;&#10;&lt;figure class="wp-block-image size-large"&gt;&lt;img loading="lazy" decoding="async" width="1024" height="307" src="https://static.digihunch.com/wp-content/uploads/2023/01/Kubernetes-Admission-controllers-01-flow-diagram-1024x307.jpeg" alt="" class="wp-image-7733" srcset="https://static.digihunch.com/wp-content/uploads/2023/01/Kubernetes-Admission-controllers-01-flow-diagram-1024x307.jpeg 1024w, https://static.digihunch.com/wp-content/uploads/2023/01/Kubernetes-Admission-controllers-01-flow-diagram-300x90.jpeg 300w, https://static.digihunch.com/wp-content/uploads/2023/01/Kubernetes-Admission-controllers-01-flow-diagram-768x231.jpeg 768w, https://static.digihunch.com/wp-content/uploads/2023/01/Kubernetes-Admission-controllers-01-flow-diagram-1536x461.jpeg 1536w, https://static.digihunch.com/wp-content/uploads/2023/01/Kubernetes-Admission-controllers-01-flow-diagram-2048x615.jpeg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The mutating admission hook takes action to change the API request, whereas the validating admission hook accepts or denies the request. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;A good example of mutating webhook is Istio&amp;#8217;s sidecar injector. We can view the configuration with this command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ kubectl get MutatingWebhookConfiguration istio-sidecar-injector -o yaml | less&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;From the manifest returned, we can see that in this configuration the request is forwarded to istiod service on port 443, at path /inject for processing. We can also see some matching rules to find the target Pod creation API request.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Validating webhook can be display with the following call:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ kubectl get ValidatingWebhookConfiguration&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;The output of validating webhook is a yes or no decision. We usually use validating webhook in conjunction with a policy engine to decide whether the request should be accepted or denied.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading" id="open-policy-agent"&gt;Open Policy Agent&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Open Policy Agent (OPA) is an open-source general-purpose policy engine that applies policies written in &lt;a href="https://www.openpolicyagent.org/docs/latest/policy-language/"&gt;Rego language&lt;/a&gt; to ingested JSON document and returns a result. It is usually integrated with system which requires a policy engine. For example, &lt;a href="https://kyverno.io/"&gt;Kyverno&lt;/a&gt; is a policy engine designed for Kubernetes. &lt;a href="https://blog.styra.com/blog/authorize-better-istio-traffic-policies-with-opa-styra-das"&gt;Styra&lt;/a&gt; (one of the OPA contributors) develops policy engines to integrate with Istio&amp;#8217;s authorization policy. They have &lt;a href="https://academy.styra.com/"&gt;online courses&lt;/a&gt; on &lt;a href="https://academy.styra.com/courses/opa-rego"&gt;OPA policy authoring&lt;/a&gt; and &lt;a href="https://academy.styra.com/courses/microservice"&gt;microservice authorization&lt;/a&gt; with their product.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;OPA is build to be a general-purpose, unified way of solving policy and authorization problem. With microservice authorization, the activities includes decision making (determine action based on input, aka Policy Decision Point, PDP), and decision enforcement (issue 400 code or 200 code depending on decision, aka Policy Enforcement Point, PEP). OPA is introduced to decouple these two activities. OPA&amp;#8217;s input is a JSON payload and it uses Policy in Rego language to come to decision.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The team that developers Open Policy Agent also created their controller (with OPA as the core component) to run validating web hook and mutating web hook. The original version is OPA-Kubernetes that uses kube-mgmt. This original version is also dubbed Gatekeeper v1.0. When OPA starts, the kube-mgmt sidecar container will load Kubernetes Namespace and Ingress objects into OPA. You can configure the sidecar to load any kind of Kubernetes object into OPA. The sidecar establishes watches on the Kubernetes API server so that OPA has access to an eventually consistent cache of Kubernetes objects. It has gone through a couple of major version changes as summarized in &lt;a href="https://kubernetes.io/blog/2019/08/06/opa-gatekeeper-policy-and-governance-for-kubernetes/#evolution"&gt;this&lt;/a&gt; section. As of today, when we deploy Gatekeeper we should use version 3.&lt;/p&gt;&#10;&lt;h2 class="wp-block-heading" id="gatekeeper-v3"&gt;Gatekeeper v3&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Currently, Gatekeeper v3 is the most popular choice for Kubernetes Policy Controller. The diagram bellow illustrate how Gatekeeper integrates with Kubernetes API server.&lt;/p&gt;&#10;&lt;figure class="wp-block-image size-full"&gt;&lt;img loading="lazy" decoding="async" width="943" height="478" src="https://static.digihunch.com/wp-content/uploads/2022/02/apiserver.png" alt="" class="wp-image-3481"/&gt;&lt;figcaption class="wp-element-caption"&gt;Gatekeeper and Kubernetes&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;We can follow &lt;a href="https://open-policy-agent.github.io/gatekeeper/website/docs/install/"&gt;this&lt;/a&gt; guide to install Gatekeeper but the key step is as simple as to apply the correct version of manifest. Alternatively it can be installed &lt;a href="https://open-policy-agent.github.io/gatekeeper/website/docs/install#deploying-via-helm"&gt;using Helm&lt;/a&gt;. After the installation, we should see a Service named &lt;em&gt;gatekeeper-webhook-service&lt;/em&gt; in the &lt;em&gt;gatekeeper-system&lt;/em&gt; namespace. We can also inspect the newly created validationg web hook configuration&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;k get validatingwebhookconfiguration gatekeeper-validating-webhook-configuration -o yaml | less&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;The result indicates that the configuration forwards incoming manifests to the &lt;em&gt;gatekeeper-webhook-service&lt;/em&gt; web service at the path /v1/admin for validation, and then at /v1/admitlabel for labelling. The configuration also stores rules as matching criteria.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;We can smoke test Gatekeeper v3, with the basic example in its &lt;a href="https://github.com/open-policy-agent/gatekeeper/tree/master/example"&gt;directory&lt;/a&gt;. Apply the template, constraint and then the manifests in resources. The pod creation will fail with an error like:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Error from server &lt;span style="color:#f92672"&gt;([&lt;/span&gt;pod-must-have-gk&lt;span style="color:#f92672"&gt;]&lt;/span&gt; you must provide labels: &lt;span style="color:#f92672"&gt;{&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;gatekeeper&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;})&lt;/span&gt;: error when creating &lt;span style="color:#e6db74"&gt;&amp;#34;resources/bad_pod_namespaceselector.yaml&amp;#34;&lt;/span&gt;: admission webhook &lt;span style="color:#e6db74"&gt;&amp;#34;validation.gatekeeper.sh&amp;#34;&lt;/span&gt; denied the request: &lt;span style="color:#f92672"&gt;[&lt;/span&gt;pod-must-have-gk&lt;span style="color:#f92672"&gt;]&lt;/span&gt; you must provide labels: &lt;span style="color:#f92672"&gt;{&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;gatekeeper&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;The gatekeeper &lt;a href="https://open-policy-agent.github.io/gatekeeper/website/docs/howto"&gt;document&lt;/a&gt; also covers the details of using ConstraintTemplate and Constraints. However, Writing your own a policy in Rego still takes time and we want to piggyback on the community for commonly used policies. &lt;a href="https://github.com/open-policy-agent"&gt;OPA&lt;/a&gt;&amp;#8216;s &lt;a href="https://github.com/open-policy-agent/gatekeeper-library"&gt;gatekeeper-library&lt;/a&gt; projects keeps a handful of those in its &lt;a href="https://github.com/open-policy-agent/gatekeeper-library/tree/master/library"&gt;library&lt;/a&gt; directory. We can test the &lt;a href="https://github.com/open-policy-agent/gatekeeper-library/tree/master/library/pod-security-policy/privileged-containers"&gt;privileged container&lt;/a&gt; example:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ cd gatekeeper-library/library/pod-security-policy/privileged-containers&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ kustomize build . | kubectl apply -f -&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;constrainttemplate.templates.gatekeeper.sh/k8spspprivilegedcontainer created&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ kubectl apply -f samples/psp-privileged-container/example_disallowed.yaml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pod/nginx-privileged-disallowed created&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ kubectl delete -f samples/psp-privileged-container/example_disallowed.yaml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pod &lt;span style="color:#e6db74"&gt;&amp;#34;nginx-privileged-disallowed&amp;#34;&lt;/span&gt; deleted&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ kubectl apply -f samples/psp-privileged-container/constraint.yaml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;k8spspprivilegedcontainer.constraints.gatekeeper.sh/psp-privileged-container created&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ kubectl apply -f samples/psp-privileged-container/example_disallowed.yaml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Error from server &lt;span style="color:#f92672"&gt;([&lt;/span&gt;psp-privileged-container&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Privileged container is not allowed: nginx, securityContext: &lt;span style="color:#f92672"&gt;{&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;privileged&amp;#34;&lt;/span&gt;: true&lt;span style="color:#f92672"&gt;})&lt;/span&gt;: error when creating &lt;span style="color:#e6db74"&gt;&amp;#34;samples/psp-privileged-container/example_disallowed.yaml&amp;#34;&lt;/span&gt;: admission webhook &lt;span style="color:#e6db74"&gt;&amp;#34;validation.gatekeeper.sh&amp;#34;&lt;/span&gt; denied the request: &lt;span style="color:#f92672"&gt;[&lt;/span&gt;psp-privileged-container&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Privileged container is not allowed: nginx, securityContext: &lt;span style="color:#f92672"&gt;{&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;privileged&amp;#34;&lt;/span&gt;: true&lt;span style="color:#f92672"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p class="wp-block-paragraph"&gt;Currently the library &lt;a href="https://github.com/open-policy-agent/gatekeeper-library/tree/master/library"&gt;directory&lt;/a&gt; contains two sub-directories, general and pod-scurity-policy. The latter is to regulate Pod creation, while the former includes more common usecases such as disable node port, enforce https, and enforce probes. This is the place I start with when building a policy.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The policy constraints take effect cluster wide. When we have multiple clusters, we would like a unified place to manage policies. &lt;/p&gt;&#10;&lt;h2 class="wp-block-heading" id="azure-policy-with-aks"&gt;Azure Policy with AKS&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;We take Azure Policy with AKS as an example to illustrate how public cloud platform can simplify policy management. When building AKS cluster, an &lt;a href="https://docs.microsoft.com/en-us/azure/governance/policy/concepts/policy-for-kubernetes#install-azure-policy-add-on-for-aks"&gt;addon&lt;/a&gt; profile for Azure Policy can be installed. This allows Azure Policy to connect to the AKS cluster. Azure Policy contains many built-in policies definitions (as well as initiative definitions which are groups of related policies). We can simply search by Kubernetes keyword and look for the built-in policies. For example, there is a built-in policy definition &amp;#8220;Kubernetes clusters should not allow container privilege escalation. The definitions (policy or initiative) can be assigned to a resource group with enforcement action set to denied, and with excluded namespaces, as shown in the screenshot below&lt;/p&gt;&#10;&lt;figure class="wp-block-image size-large"&gt;&lt;img loading="lazy" decoding="async" width="1566" height="1056" src="https://static.digihunch.com/wp-content/uploads/2022/02/image-2.png" alt="" class="wp-image-3497"/&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The assignment can take as long as 10 minutes to push down to the cluster. Then we should be able to confirm by checking the constraint CRDs. We can see this This setup brings a centralized policy management system that can be easily hooked up to multiple clusters.&lt;/p&gt;&#10;&lt;figure class="wp-block-image"&gt;&lt;img decoding="async" src="https://ahmedkhamessi.com/img/azurepolicy/azurepolicy.png" alt="Azure Policy and OPA Gatekeeper underlay for AKS"/&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Other benefits of this architecture includes the ability to report compliance. As per CIS report for Azure AKS recommendation 4.3:&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Azure Policy extends Gatekeeper v3, an admission controller webhook for Open Policy Agent (OPA), to apply at-scale enforcements and safeguards on your clusters in a centralized, consistent manner. It covers many &lt;a href="https://static.digihunch.com/2021/01/basic-kubernetes-resource-object-1-of-2/"&gt;basic resource&lt;/a&gt; &lt;a href="https://static.digihunch.com/2021/02/basic-resource-object-in-kubernetes-2-of-2/"&gt;types&lt;/a&gt; but does not cover any well-known CRDs. Azure Policy makes it possible to manage and report on the compliance state of your Kubernetes clusters from one place.&amp;nbsp;&lt;/p&gt;&#10;&lt;ul class="wp-block-list"&gt;&#10;&lt;li&gt;Checks with Azure Policy service for policy assignments to the cluster.&lt;/li&gt;&#10;&lt;li&gt;Deploys policy definitions into the cluster as constraint template and constraint custom resources.&lt;/li&gt;&#10;&lt;li&gt;Reports auditing and compliance details back to Azure Policy service.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p class="wp-block-paragraph"&gt;As of February 2022, AWS EKS doesn&amp;#8217;t seem to have the equivalent of this capability to integrate with a policy management. The only option would be to install Gatekeeper v3 &lt;a href="https://aws.amazon.com/blogs/opensource/using-open-policy-agent-on-amazon-eks/"&gt;yourself&lt;/a&gt; on the cluster, or host it separately. &lt;/p&gt;&#10;&lt;h2 class="wp-block-heading" id="bottom-line"&gt;Bottom line&lt;/h2&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Admission control should be a standard setup in Kubernetes deployment. When building gatekeeper system on your own, it can be set up separately on a different cluster. When Kubernetes is provided as a platform, it is very helpful for platform operator to manage their tenants. If the tenant is application development team, it also makes sense for them to develop their own policies for the developers in their team.&lt;/p&gt;&#10;&lt;nav class="wp-post-navigation" aria-label="Post navigation"&gt;&#10;&lt;a rel="prev" href="https://static.digihunch.com/2021/12/from-ingress-to-gateway-why-you-need-istio-gateways-on-kubernetes-platforms/"&gt;&lt;span class="wp-post-navigation-label"&gt;Previous Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;From Ingress to CRD: why my solution needs Istio Gateways on Kubernetes platforms&lt;/strong&gt;&lt;/a&gt;&#10;&lt;a rel="next" href="https://static.digihunch.com/2022/01/fluxcd-continuous-deployment-with-gitops/"&gt;&lt;span class="wp-post-navigation-label"&gt;Next Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;FluxCD: Continuous Deployment with GitOps&lt;/strong&gt;&lt;/a&gt;&#10;&lt;/nav&gt;&#10;</description></item><item><title>Basic Resource Object in Kubernetes 1 of 2</title><link>https://static.digihunch.com/2021/01/basic-kubernetes-resource-object-1-of-2/</link><pubDate>Sat, 16 Jan 2021 22:13:00 -0400</pubDate><guid>https://static.digihunch.com/2021/01/basic-kubernetes-resource-object-1-of-2/</guid><description>&lt;p class="wp-block-paragraph"&gt;For someone from a system administration background, it would be amazing to discover that Kubernetes provides a solution to every pain point in the traditional software deployment landscape. On the contrary, it also brings about a lot of complexity due to the types of resource objects introduced. &lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&lt;figure class="aligncenter"&gt;&lt;img decoding="async" src="https://github.com/kubernetes/community/raw/master/icons/png/resources/labeled/pod-128.png" alt=""/&gt;&lt;figcaption&gt;Pod&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;A Pod is a shared execution environment for one or more containers. The containers running in a Pod share resources such as memory, volumes, network namespace (e.g. IP address, port range, hostname, routing table), UTS namespace (e.g. hostname) and IPC namespace (Unix domain sockets). Every Pod has its own IP address that is routable on the Pod network. All Pods connect to the same flat network called the Pod network.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;A pod most commonly only contains a single container, which is considered a good practice, unless there is good reasons to put two containers in a single pod (sharing resource). One such good reason is to co-schedule tightly-coupled workloads (such as logging, sharing volume, etc). Within the Pod, the containers communicate with each other via localhost interface of the Pod. In service mesh model, there is also a proxy container in each application Pod. The proxy container handles all network traffic entering and leaving the Pod. Also, within the Pod, to avoid competing for resources, individual containers can have their own cgroup limits, which actively police resource usage.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Pods are mortal (composable). They come and go (with dynamic IPs), so application should not store state in Pods. Deploying a Pod is an atomic (all or nothing) operation. When a Pod is scheduled to a node, it enters the pending state while the container runtime on the node downloads images and starts any containers. Once&amp;#8217;s everything is ready, the Pod enters the running state.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;We typically deploy Pods via higher-level controllers such as Deployments (to offer scalability and rolling updates), DaemonSets (to run one instance of a service on every node in the cluster), StatefulSets (for stateful application components), and CronJobs (for short-lived tasks that need to run at set times just like a Linux &lt;a href="https://static.digihunch.com/2018/05/cron-and-logrotate-in-centos/"&gt;cronjob&lt;/a&gt;).&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&lt;figure class="aligncenter"&gt;&lt;img decoding="async" src="https://github.com/kubernetes/community/raw/master/icons/png/resources/labeled/deploy-128.png" alt=""/&gt;&lt;figcaption&gt;Deployments&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Deployment manages multiple replicas of the same Pod (via ReplicaSets). To follow best practice, you interact with Deployments instead of ReplicaSets, and use YAML file (declarative model). You can perform rolling update or rollback.&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&lt;figure class="aligncenter"&gt;&lt;img decoding="async" src="https://github.com/kubernetes/community/raw/master/icons/png/resources/labeled/rs-128.png" alt=""/&gt;&lt;figcaption&gt;ReplicaSets&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;ReplicaSets provide self-healing and scaling capabilities to Pods. If a Pod fails, it will be replaced. If load increases, then the ReplicaSets creates new Pod. This is all implemented with a background reconciliation loop that is constantly checking whether the right number of Pod replicas are present on the cluster. If not, Kubernetes declares a red-alert condition, orders the control plan to bring up more replicas. The best practice however, is that you should not manage ReplicaSets directly. Instead, you should perform all actions against the Deployment object and leave the Deployment to manage ReplicaSets.&lt;/p&gt;&#10;&lt;figure class="wp-block-image"&gt;&lt;img decoding="async" src="https://storage.googleapis.com/cdn.thenewstack.io/media/2017/11/07751442-deployment.png" alt=""/&gt;&lt;/figure&gt;&#10;&lt;p class="wp-block-paragraph"&gt;&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&lt;figure class="aligncenter"&gt;&lt;img decoding="async" src="https://github.com/kubernetes/community/raw/master/icons/png/resources/labeled/svc-128.png" alt=""/&gt;&lt;figcaption&gt;Service&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Pods themselves are mortal (IP churn) so it&amp;#8217;s a bad idea to talk directly to individual Pods. Service object provides stable and reliable networking for a set of dynamic Pods. Service gets its own stable IP address, stable port and stable DNS name. It can also load-balance request across the Pods.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Services are loosely coupled with Pods via labels and label selectors. You specify label selector for Service and labels on Pods when creating them. All the labels in label selector are used to select target Pods. Service acts as front-end, consisting of stable IP, DNS name and port, with Pods acting as backend, consisting of constantly changing Pods. Labels are simple yet extremely powerful. During blue-green update, you may use version label as a technique to control what backend pool is used behind Service object. For example, start with version=1, deploy version 2, remove version from label selector, and eventually add version=2 back to label selector, before phasing out the old Deployment.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Services learn Pod status via Endpoint object, more details to follow.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;There are several types of Service, the default being &lt;strong&gt;ClusterIP&lt;/strong&gt;. A ClusterIP Service has a stable IP address and port that is only accessible from inside the cluster. The ClusterIP gets registered against the name of the Service on the cluster&amp;#8217;s internal DNS service (implemented via coreDNS with Control plane Pods). This means that the ClusterIP only works within the cluster, not outside. The other type of Service is called a &lt;strong&gt;NodePort&lt;/strong&gt;, which is built on top of ClusterIP, but also enables access from outside of the cluster. The Service object has a reliable NodePort mapped to every node in the cluster. The NodePort value is the same on every cluster. Traffic from outside of the cluster can hit any node in the cluster on the NodePort and get through the the Pods.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Other types of Services include LoadBalancer and ExternalName. LoadBalancer Services integrate with load-balancers from cloud provider. They build on top of NodePort Services and allow clients on the internet to reach your Pods via the load balancer of cloud vendor. ExternalName Services route traffic to systems outside of your K8s cluster.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;For service discovery within the cluster, Kubelet program every container with the knowledge of the internal DNS (/etc/resolv.conf). The internal DNS service watches constantly the API server for new Services and automatically register them in the DNS. The other means of service discovery is through environment variables. However, in this method the Pods have no way of learning about new Services added to the cluster after the Pod itself is created.&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&lt;figure class="aligncenter"&gt;&lt;img decoding="async" src="https://github.com/kubernetes/community/raw/master/icons/png/resources/labeled/ep-128.png" alt=""/&gt;&lt;figcaption&gt;Endpoints&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Endpoints object is a dynamic list of all the healthy Pods on the cluster that match the Service&amp;#8217;s label selector. Each Service gets its own Endpoints objects for an up-to-date list of matching Pods. Kubernetes is constantly evaluating the Service&amp;#8217;s label selector against the currently list of healthy Pods on the cluster. Any new Pods that match the selector get added to the Endpoints object, and any Pods that disappear get removed.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;When sending traffic to Pods, via a Service, an application will query the cluster&amp;#8217;s internal DNS for the IP address of a Service, then sends the traffic to this stable IP address. Service then forwards it on to a Pod. Kubernetes-native application however, has the ability to query the Endpoints API directly, bypassing the DNS lookup and use of the Service&amp;#8217;s IP.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;It requires a thorough understanding of Services, Endpoints and the service discovery mechanism to perform effective troubleshooting in Kubernetes.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The aforementioned internal DNS service (we usually call it the &amp;#8220;cluster DNS&amp;#8221;) is implemented in the kube-system Namespace as a set of Pods managed by a Deployment called coredns. These Pods are fronted by a Service called kube-dns. The cluster DNS is constantly looking for new Services and automatically register their details (metadata.name). We might need to check the logs for each of the coredns Pods during troubleshooting. &lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The kubelet process on every node is watching the API Server for new Endpoints objects, when it sees them, it creates local networking rules that redirect ClusterIP traffic to Pod IPs, using &lt;a href="https://static.digihunch.com/2020/11/ipvs-iptables-and-kube-proxy/" class="rank-math-link"&gt;IPVS technology&lt;/a&gt; on Linux to manage these rules.&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&lt;figure class="aligncenter"&gt;&lt;img decoding="async" src="https://github.com/kubernetes/community/raw/master/icons/png/resources/labeled/ds-128.png" alt=""/&gt;&lt;figcaption&gt;DaemonSet&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;A DaemonSet ensures that all (or some) Nodes run a copy of a Pod. As nodes are added to the cluster, Pods are added to them. As nodes are removed from the cluster, those Pods are garbage collected. Deleting a DaemonSet will clean up the Pods it created.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Some typical uses of a DaemonSet are: cluster storage daemon on every node, logs collection daemon on every node, a node monitoring daemon on every node.&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&lt;figure class="aligncenter"&gt;&lt;img decoding="async" src="https://github.com/kubernetes/community/raw/master/icons/png/resources/labeled/hpa-128.png" alt=""/&gt;&lt;figcaption&gt;Horizontal Pod Autoscaler&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The Horizontal Pod Autoscaler automatically scales the number of Pods in a replication controller, deployment, replica set or stateful set based on observed CPU utilization (or, with custom metrics support, on some other application-provided metrics). Note that Horizontal Pod Autoscaling does not apply to objects that can&amp;#8217;t be scaled, for example, DaemonSets.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;The Horizontal Pod Autoscaler is implemented as a Kubernetes API resource and a controller. The resource determines the behaviour of the controller. The controller periodically adjusts the number of replicas in a replication controller or deployment to match the observed average CPU utilization to the target specified by user.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;There are more details about HPA &lt;a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/" class="rank-math-link"&gt;here&lt;/a&gt; and &lt;a href="https://cloud.google.com/kubernetes-engine/docs/concepts/horizontalpodautoscaler" class="rank-math-link"&gt;here&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&lt;figure class="aligncenter"&gt;&lt;img decoding="async" src="https://github.com/kubernetes/community/blob/master/icons/png/resources/labeled/sts-128.png?raw=true" alt="sts-128.png"/&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;StatefulSets are designed for stateful application, which creates and saves valuable data. The three properties that form the state of a Pod are:&lt;/p&gt;&#10;&lt;ul class="wp-block-list"&gt;&lt;li&gt;Pod names (&amp;lt;StatefulSetName&amp;gt;-&amp;lt;Integer&amp;gt;)&lt;/li&gt;&lt;li&gt;DNS hostnames&lt;/li&gt;&lt;li&gt;volume bindings&lt;/li&gt;&lt;/ul&gt;&#10;&lt;p class="wp-block-paragraph"&gt;They are sometimes referred to as the Pods &lt;em&gt;sticky ID&lt;/em&gt;. StatefulSets ensures that these are all predictable and persistent. For example, failed Pods managed by a StatefulSet will be replaced by new Pods with the exact same Pod name, the exact same DNS hostname, and the exact same volumes, even if the replacement Pod is started on a different cluster Node.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Note that StatefulSets create one Pod at a time, and always wait for previous Pods to be &lt;em&gt;running and ready&lt;/em&gt; before creating the next. Scaling operations are also governed by the same ordered startup rules. This is different from Deployments that use a ReplicaSet controller to start all Pods at the same time, causing potential race conditions. The way StatefulSet controllers do their own self-healing and scaling is architecturally different to Deployments which use a separate ReplicaSet controller for these operations. The reason it is a game changer to know the order in which Pods will be scaled down, as well as that Pods will not be terminated in parallel, is because clustered apps that store data are usually at high risk of losing data if multiple replicas go down at the same time.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Deleting a StatefulSet does not terminate Pods in order. So you may want to scale a StatefulSet to 0 replicas before deleting it. You might also set 10 seconds grace period before terminating to allow applications a chance to flush local buffers and safely commit any writes still in flight.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;In Kubernetes, Volumes are decoupled from Pods via PersistentVolumes and PersistentVolumeClaims. So volumes have separate lifecycles to Pods and can survive Pod failures and termination operations. When a StatefulSet Pod is created, any volumes it needs are created at the same time and named in a way to connect them to the right Pod. Any time a StatefulSet Pod fails or is terminated, the associated volumes are unaffected. This allows replacement Pods to attach to the same storage as the Pods they&amp;#8217;re replacing, even if the replacement Pod is scheduled to a different cluster Node. Similarly, if a StatefulSet Pod is detected as part of a scale-down operation, subsequent scale-up operations will attach new Pods to the existing volumes that match their names.&lt;/p&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Since each StatefulSet Pod needs its own unique storage, hence its own PVC, this can be done by volumeClaimTemplate, which dynamically creates a PVC each time a new Pod replica is dynamically created. This eliminates the hassle to have to pre-create a unique PVC for every potential StatefulSet Pod.&lt;/p&gt;&#10;&lt;div class="wp-block-image"&gt;&lt;figure class="aligncenter"&gt;&lt;img decoding="async" src="https://github.com/kubernetes/community/raw/master/icons/png/resources/labeled/ns-128.png" alt=""/&gt;&lt;figcaption&gt;Namespaces&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;p class="wp-block-paragraph"&gt;Namespaces allows you to partition resource objects. For example, you may create a Namespace called prod and dev. Object names must be unique within Namespaces but not across Namespaces.&lt;/p&gt;&#10;&lt;nav class="wp-post-navigation" aria-label="Post navigation"&gt;&#10;&lt;a rel="prev" href="https://static.digihunch.com/2020/12/ansible-tower-lab-environment-on-aws/"&gt;&lt;span class="wp-post-navigation-label"&gt;Previous Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;AWS CDK example in Typescript – provision an AWX server&lt;/strong&gt;&lt;/a&gt;&#10;&lt;a rel="next" href="https://static.digihunch.com/2021/01/blockchain-and-di-fi/"&gt;&lt;span class="wp-post-navigation-label"&gt;Next Post&lt;/span&gt;&lt;strong class="wp-post-navigation-title"&gt;Blockchain and DeFi&lt;/strong&gt;&lt;/a&gt;&#10;&lt;/nav&gt;&#10;</description></item></channel></rss>