<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vandebron</title>
    <description>The latest articles on DEV Community by Vandebron (@vandebron).</description>
    <link>https://dev.to/vandebron</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2841%2F991d0978-8bc2-472a-bce5-2d6cf947b328.png</url>
      <title>DEV Community: Vandebron</title>
      <link>https://dev.to/vandebron</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vandebron"/>
    <language>en</language>
    <item>
      <title>Replacing App Center with GitHub Actions</title>
      <dc:creator>John Fisher</dc:creator>
      <pubDate>Sat, 22 Feb 2025 13:03:27 +0000</pubDate>
      <link>https://dev.to/vandebron/replacing-app-center-with-github-actions-emd</link>
      <guid>https://dev.to/vandebron/replacing-app-center-with-github-actions-emd</guid>
      <description>&lt;h2&gt;
  
  
  Why GHA?
&lt;/h2&gt;

&lt;p&gt;This seems like a lot of work... Why not go with an off-the-shelf solution from something like Bitrise or Codemagic? Fair question but not the main point of this post. To look into our reasoning check out the Benefits section!&lt;/p&gt;

&lt;h2&gt;
  
  
  Some notes before we get started
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;We use this workflow to build two apps, one of which can be white-labeled, so we have additional &lt;code&gt;app&lt;/code&gt; and &lt;code&gt;white-label-release&lt;/code&gt; inputs which makes things a bit trickier. You might not need that so feel free to trim stuff down and make it your own! But for us, we why we have the &lt;code&gt;env-variable-prep-android.sh&lt;/code&gt; which normalizes the variable names used for secrets so those secrets and build file names, etc. can be easily used. If you just have a single app you probably don't need this script.&lt;/li&gt;
&lt;li&gt;The code below is only for the build process. Though the Architecture Decision Record (ADR) considered how this would affect future decisions about artifact upload automation and rolling out releases for internal testing, nothing about that is automated here.&lt;/li&gt;
&lt;li&gt;There's a lot of code here. We removed version numbers to make sure we're not giving out too much information. If you see something like &lt;code&gt;@vx.x.x&lt;/code&gt;, you'll have to fill those in with the versions that are needed for your app/pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Part 1 - Basic Setup
&lt;/h3&gt;

&lt;p&gt;Add the files below. Nothing in this setup should effect App Center but it is good to check in your changes to a branch and test those against the regular App Center build flow. The main point of the steps in this section is to just get a basic action, with inputs, set up and running. Full implementation is further down. The &lt;code&gt;env-variable-prep-android.sh&lt;/code&gt; and &lt;code&gt;env-variable-prep-ios.sh&lt;/code&gt; files created variables based off the input parameters passed in so they can be used in a reusable and consistent manner later on. The &lt;code&gt;env-file-prep.sh&lt;/code&gt; file is responsible for creating a &lt;code&gt;.env&lt;/code&gt; file with values from whichever environment you're using. Though it's nice to add the &lt;code&gt;Fastlane&lt;/code&gt; and &lt;code&gt;Gymfile&lt;/code&gt; files, those won't be used until Step 4 when the rest of the pipeline is fleshed out.&lt;/p&gt;

&lt;h4&gt;
  
  
  Android
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;file: .github/workflows/mobile-apps-build-android.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mobile App Build for Android&lt;/span&gt;
  &lt;span class="na"&gt;run-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Build:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Android,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}},&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.environment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}},&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;white-label-release=${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.white-label-release&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;App'&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-first-app&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-second-app&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Environment'&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;acceptance&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
        &lt;span class="na"&gt;white-label-release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;White-labeled&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Release?'&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
          &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build app for android&lt;/span&gt;
      &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.environment }}&lt;/span&gt;
      &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout ${{ github.repository }}&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@vx.x.x&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Echo Input&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;APP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.app }}&lt;/span&gt;
            &lt;span class="na"&gt;WHITE_LABEL_RELEASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.white-label-release }}&lt;/span&gt;
            &lt;span class="na"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.environment }}&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;echo "APP=$APP"&lt;/span&gt;
            &lt;span class="s"&gt;echo "WHITE_LABEL_RELEASE=$WHITE_LABEL_RELEASE"&lt;/span&gt;
            &lt;span class="s"&gt;echo "ENVIRONMENT=$ENVIRONMENT"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;file: env-variable-prep-android.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
  &lt;span class="nv"&gt;ARTIFACT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"mobile-my-first-app-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-release"&lt;/span&gt;
  &lt;span class="nv"&gt;FASTLANE_ANDROID_FLAVOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"artifact-name=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARTIFACT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fastlane-android-flavor=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FASTLANE_ANDROID_FLAVOR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="c"&gt;# Secrets should not get sent out over tee command (which also prints it to console)&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"android-key-alias=my-first-app"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"android-keystore-file=../keystores/my-first-app.jks"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"android-keystore-pass=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ANDROID_KEY_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"android-store-pass=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ANDROID_STORE_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;file: ./fastlane/Fastfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  opt_out_usage
  platform :android &lt;span class="k"&gt;do
    &lt;/span&gt;desc &lt;span class="s1"&gt;'Build app for Android'&lt;/span&gt;
      lane :build &lt;span class="k"&gt;do
        &lt;/span&gt;build_android_app&lt;span class="o"&gt;(&lt;/span&gt;
          task: &lt;span class="s1"&gt;'bundle'&lt;/span&gt;,
          flavor: ENV[&lt;span class="s1"&gt;'FASTLANE_ANDROID_FLAVOR'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;,
          build_type: &lt;span class="s1"&gt;'Release'&lt;/span&gt;,
          project_dir: &lt;span class="s1"&gt;'android/'&lt;/span&gt;,
          print_command: &lt;span class="nb"&gt;true&lt;/span&gt;,
          print_command_output: &lt;span class="nb"&gt;true&lt;/span&gt;,
        &lt;span class="o"&gt;)&lt;/span&gt;
      end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  iOS
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;file: .github/workflows/mobile-apps-build-ios.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="s"&gt;---&lt;/span&gt;
  &lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mobile App Build for iOS&lt;/span&gt;
  &lt;span class="s"&gt;run-name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Build:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;iOS,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}},&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.environment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}},&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;white-label-release=${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.white-label-release&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;App'&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-first-app&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-second-app&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Environment'&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;acceptance&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
        &lt;span class="na"&gt;white-label-release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;White-labeled&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Release?'&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
          &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build app for iOS&lt;/span&gt;
      &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-latest&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.environment }}&lt;/span&gt;
      &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout ${{ github.repository }}&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@vx.x.x&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Echo Input&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;APP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.app }}&lt;/span&gt;
            &lt;span class="na"&gt;WHITE_LABEL_RELEASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.white-label-release }}&lt;/span&gt;
            &lt;span class="na"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.environment }}&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;echo "APP=$APP"&lt;/span&gt;
            &lt;span class="s"&gt;echo "WHITE_LABEL_RELEASE=$WHITE_LABEL_RELEASE"&lt;/span&gt;
            &lt;span class="s"&gt;echo "ENVIRONMENT=$ENVIRONMENT"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;file: env-variable-prep-ios.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
  &lt;span class="c"&gt;# Secrets should not get sent out over tee command (which also prints it to console)&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ios-mobile-provisioning-profile=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IOS_MOBILE_PROVISIONING_PROFILE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;file: ./fastlane/Gymfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  scheme&lt;span class="o"&gt;(&lt;/span&gt;ENV[&lt;span class="s1"&gt;'SCHEME'&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;
  workspace&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ios/my-first-app.xcworkspace"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  export_options&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ios/exportOptions.plist"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  output_directory&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ios/build"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  output_name&lt;span class="o"&gt;(&lt;/span&gt;ENV[&lt;span class="s1"&gt;'SCHEME'&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Both Android &amp;amp; iOS
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;file: env-file-prep.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
  &lt;span class="c"&gt;#####################################################&lt;/span&gt;
  &lt;span class="c"&gt;# Creates an .env file for use in react-native-config&lt;/span&gt;
  &lt;span class="c"&gt;# This script should be run from the /mobile folder&lt;/span&gt;
  &lt;span class="c"&gt;#####################################################&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"acceptance"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ENVIRONMENT is not set, using 'production' as default."&lt;/span&gt;
      &lt;span class="nv"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Copying 'my-first-app/.env.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' to 'my-first-app/.env'..."&lt;/span&gt;
  &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"my-first-app/.env.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"my-first-app/.env"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Success!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 2 - Testing your Workflow Locally
&lt;/h3&gt;

&lt;p&gt;WARNING: GitHub Actions doesn't let you run a &lt;code&gt;workflow_dispatch&lt;/code&gt; action until it has been merged into the main branch so you'll want to get the basic setup above in place before you start tinkering with application logic or things that could effect App Center. Also note that while &lt;code&gt;act&lt;/code&gt; is helpful for getting the basics in place, it's unlikely you'll be able to test the complete process locally because, at least for us a) Android pipeline crashes halfway through the &lt;code&gt;Build App&lt;/code&gt; step with an error of &lt;code&gt;Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed)&lt;/code&gt;, and b) iOS tries to install a fresh copy of Xcode&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Follow installation instructions on &lt;a href="https://nektosact.com/installation/index.html" rel="noopener noreferrer"&gt;their User Guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a &lt;code&gt;~/.actrc&lt;/code&gt; file that looks like this&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nt"&gt;--container-architecture&lt;/span&gt; linux/amd64
  &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="nv"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_TOKEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Get your &lt;code&gt;$GITHUB_TOKEN&lt;/code&gt; env variable in place&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;a href="https://cli.github.com/" rel="noopener noreferrer"&gt;gh, the GitHub cli&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Modify shells (zsh shown below)&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gh auth token&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Close and re-open your terminal. Test to make sure you can see your &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; envrionment variable&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Set up a temporary local file for secrets. WARNING: Once you’re done, remember to delete the file so it’s not hanging around on your system!! (or just don’t create it in the first place unless you really need it)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;file: .secrets&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nv"&gt;ANDROID_STORE_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"someSecr3ts"&lt;/span&gt;
  &lt;span class="nv"&gt;ANDROID_KEY_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YouWantQuotesBecause*s_etc.WillScrewYouUp"&lt;/span&gt;
  &lt;span class="nv"&gt;MAPBOX_READ_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shhhhh.Its.asecret
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Set up a temporary local file for your workflow trigger inputs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;file: app-input.json&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workflow_dispatch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-first-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"white-label-release"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Run your workflow like this. As far as I know, you can only run the iOS workflow locally if you're on a mac. To do that you'll need to provide an additional parameter: &lt;code&gt;-P macos-latest=-self-hosted&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Android&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  act &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workflows&lt;/span&gt; .github/workflows/mobile-apps-build-android.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--eventpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/Documents/IT/GitHub Actions/Mobile Apps/app-input.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/Documents/IT/GitHub Actions/Mobile Apps/.secrets"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;iOS&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  act &lt;span class="nt"&gt;-P&lt;/span&gt; macos-latest&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-self-hosted&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workflows&lt;/span&gt; .github/workflows/mobile-apps-build-ios.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--eventpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/Documents/IT/GitHub Actions/Mobile Apps/app-input.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/Documents/IT/GitHub Actions/Mobile Apps/.secrets"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 3 - Testing Things out from GHA
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Once you're happy with your changes locally, send a PR &amp;amp; merge them in! Remember this is only the basic setup to get the action avaible from the GitHub Actions interface so maybe leave a comment in the PR notifying your teammates about how they should expect a follow-up PR&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Go to the "Actions" tab at the top of your GitHub repo. You should now see your actions for building iOS and Android on the left. Since it's a &lt;code&gt;workflow_dispatch&lt;/code&gt; action you'll trigger the actions manually.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fus9hoocxv8gfhup45otb.png" alt="Run GitHub Actions workflow" width="666" height="490"&gt;&lt;br&gt;
  
&lt;h3&gt;
  
  
  Part 4 - Implement the Full Workflow for iOS and Android
&lt;/h3&gt;

&lt;p&gt;Adjust the files below. This is where you may end up needing to modify things that affect your App Center build. Try to keep them to a mimimum so you can still use App Center for builds should anything not work as expected. &lt;a href="https://fastlane.tools/" rel="noopener noreferrer"&gt;Fastlane&lt;/a&gt; is a tool that helps with automating build and release processes for mobile apps. You can think of it as a toolbox of easy-to-use wrapper functions around &lt;code&gt;gradle&lt;/code&gt; for Android, and &lt;code&gt;xcodebuild&lt;/code&gt; for iOS.&lt;/p&gt;
&lt;h4&gt;
  
  
  Android
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;file: .github/workflows/mobile-apps-build-android.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="s"&gt;---&lt;/span&gt;
  &lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mobile App Build for Android&lt;/span&gt;
  &lt;span class="s"&gt;run-name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Build:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;android,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}},&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.environment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}},&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;white-label-release=${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.white-label-release&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;App'&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-first-app&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-second-app&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Environment'&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;acceptance&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
        &lt;span class="na"&gt;white-label-release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;White-labeled&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Release?'&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
          &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build app for android&lt;/span&gt;
      &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.environment }}&lt;/span&gt;
      &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout ${{ github.repository }}&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@vx.x.x&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prep Env Variables&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prep-env-variables&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;APP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.app }}&lt;/span&gt; &lt;span class="c1"&gt;# my-first-app | my-second-app&lt;/span&gt;
            &lt;span class="na"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.environment }}&lt;/span&gt;  &lt;span class="c1"&gt;# production | test | acceptance&lt;/span&gt;
            &lt;span class="na"&gt;BRAND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.white-label-release &amp;amp;&amp;amp; 'other-company' || 'my-company' }}&lt;/span&gt;
            &lt;span class="na"&gt;APP_1_ANDROID_KEY_PASSWORD_VANDEBRON&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_1_ANDROID_KEY_PASSWORD_VANDEBRON }}&lt;/span&gt;
            &lt;span class="na"&gt;APP_1_ANDROID_STORE_PASSWORD_VANDEBRON&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_1_ANDROID_STORE_PASSWORD_VANDEBRON }}&lt;/span&gt;
            &lt;span class="na"&gt;APP_1_ANDROID_KEY_PASSWORD_WHITE_LABEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_1_ANDROID_KEY_PASSWORD_WHITE_LABEL }}&lt;/span&gt;
            &lt;span class="na"&gt;APP_1_ANDROID_STORE_PASSWORD_WHITE_LABEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_1_ANDROID_STORE_PASSWORD_WHITE_LABEL }}&lt;/span&gt;
            &lt;span class="na"&gt;APP_2_ANDROID_KEY_PASSWORD_VANDEBRON&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_2_ANDROID_KEY_PASSWORD_VANDEBRON }}&lt;/span&gt;
            &lt;span class="na"&gt;APP_2_ANDROID_STORE_PASSWORD_VANDEBRON&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_2_ANDROID_STORE_PASSWORD_VANDEBRON }}&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash ./env-variable-prep-android.sh&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create .env file&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;APP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.app }}&lt;/span&gt;
            &lt;span class="na"&gt;BRAND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.inferred-brand }}&lt;/span&gt;
            &lt;span class="na"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{inputs.environment}}&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash ./env-file-prep.sh&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enable Corepack&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;corepack enable&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup NodeJS&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;setup-node&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@vx.x.x&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;18.x&lt;/span&gt;
            &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://registry.npmjs.org&lt;/span&gt;
            &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;yarn'&lt;/span&gt;
            &lt;span class="na"&gt;cache-dependency-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile/${{ inputs.app }}/yarn.lock&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Java&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@vx.x.x&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temurin'&lt;/span&gt;
            &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
            &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gradle'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Android SDK&lt;/span&gt;  &lt;span class="c1"&gt;# sadly no caching capabilities here&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;android-actions/setup-android@vx.x.x&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;log-accepted-android-sdk-licenses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
            &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tools'&lt;/span&gt;   &lt;span class="c1"&gt;# Default is 'tools platform-tools but we don't need platform-tools for packaging'&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up ruby env&lt;/span&gt; &lt;span class="c1"&gt;# Fastlane is a "Ruby gem"&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@vx&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.3.0'&lt;/span&gt; &lt;span class="c1"&gt;# Changing this to 3.3 will give you "Your Ruby version is 3.3.5, but your Gemfile specified 3.3.0"&lt;/span&gt;
            &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Gem Bundler&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile/${{ inputs.app }}&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;gem install bundler&lt;/span&gt;
            &lt;span class="s"&gt;bundle install --quiet&lt;/span&gt;
        &lt;span class="c1"&gt;# Install Dependencies&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Yarn Install&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile/${{ inputs.app }}&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install --immutable&lt;/span&gt;
        &lt;span class="c1"&gt;# Build&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build App&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec fastlane android build&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile/${{ inputs.app }}&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.environment }}&lt;/span&gt;
            &lt;span class="na"&gt;FASTLANE_ENV_INFERRED_BRAND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.inferred-brand }}&lt;/span&gt;
            &lt;span class="na"&gt;ANDROID_KEYSTORE_FILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.android-keystore-file }}&lt;/span&gt;
            &lt;span class="na"&gt;ANDROID_KEY_ALIAS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.android-key-alias }}&lt;/span&gt;
            &lt;span class="na"&gt;ANDROID_STORE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.android-store-pass }}&lt;/span&gt;
            &lt;span class="na"&gt;ANDROID_KEY_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.android-keystore-pass }}&lt;/span&gt;
            &lt;span class="na"&gt;FASTLANE_ANDROID_FLAVOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.fastlane-android-flavor }}&lt;/span&gt;
        &lt;span class="c1"&gt;# Upload&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload application&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@vx&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{steps.prep-env-variables.outputs.artifact-name}}&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mobile/${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/android/app/build/outputs/bundle/${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;steps.prep-env-variables.outputs.fastlane-android-flavor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}Release/app-${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;steps.prep-env-variables.outputs.fastlane-android-flavor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-release.aab"&lt;/span&gt;
            &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  iOS
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;file: .github/workflows/mobile-apps-build-ios.yaml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="s"&gt;---&lt;/span&gt;
  &lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mobile App Build for iOS&lt;/span&gt;
  &lt;span class="s"&gt;run-name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Build:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;iOS,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}},&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.environment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}},&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;white-label-release=${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.white-label-release&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;App'&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-first-app&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-second-app&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Environment'&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;acceptance&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
        &lt;span class="na"&gt;white-label-release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;White-labeled&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Release?'&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
          &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build app for iOS&lt;/span&gt;
      &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-latest&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.environment }}&lt;/span&gt;
      &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout ${{ github.repository }}&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@vx.x.x&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prep Env Variables&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prep-env-variables&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;APP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.app }}&lt;/span&gt; &lt;span class="c1"&gt;# my-first-app | my-second-app&lt;/span&gt;
            &lt;span class="na"&gt;BRAND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.white-label-release &amp;amp;&amp;amp; 'other-company' || 'my-company' }}&lt;/span&gt;
            &lt;span class="na"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.environment }}&lt;/span&gt;  &lt;span class="c1"&gt;# production | test | acceptance&lt;/span&gt;
            &lt;span class="na"&gt;APP_1_MY_COMPANY_IOS_MOBILE_PROVISIONING_PROFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_1_MY_COMPANY_IOS_MOBILE_PROVISIONING_PROFILE }}&lt;/span&gt;
            &lt;span class="na"&gt;APP_1_OTHER_COMPANY_IOS_MOBILE_PROVISIONING_PROFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_1_OTHER_COMPANY_IOS_MOBILE_PROVISIONING_PROFILE }}&lt;/span&gt;
            &lt;span class="na"&gt;IOS_BUILD_CERTIFICATE_P12_OTHER_COMPANY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.IOS_BUILD_CERTIFICATE_P12_OTHER_COMPANY }}&lt;/span&gt;
            &lt;span class="na"&gt;IOS_BUILD_CERTIFICATE_P12_PASSWORD_OTHER_COMPANY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.IOS_BUILD_CERTIFICATE_P12_PASSWORD_OTHER_COMPANY }}&lt;/span&gt;
            &lt;span class="na"&gt;APP_2_IOS_MOBILE_PROVISIONING_PROFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_2_IOS_MOBILE_PROVISIONING_PROFILE }}&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash ./env-variable-prep-ios.sh&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create .env file&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;APP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.app }}&lt;/span&gt; &lt;span class="c1"&gt;# my-first-app | my-second-app&lt;/span&gt;
            &lt;span class="na"&gt;BRAND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.inferred-brand }}&lt;/span&gt;
            &lt;span class="na"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{inputs.environment}}&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash ./env-file-prep.sh&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Import Build Certificate from Secrets&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apple-actions/import-codesign-certs@vx&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;p12-file-base64&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.ios-build-certificate-p12 }}&lt;/span&gt;
            &lt;span class="na"&gt;p12-password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.ios-build-certificate-p12-password }}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Import Mobile Provisioning Profile&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nickwph/apple-provisioning-profile-action@vx.x.x&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;profile-base64&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.ios-mobile-provisioning-profile }}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup NodeJS&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;setup-node&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@vx.x.x&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20.x&lt;/span&gt;
        &lt;span class="c1"&gt;# Mapbox v10 ships with bitcode on XCode 16 which is not allowed by the App Store&lt;/span&gt;
        &lt;span class="c1"&gt;# https://github.com/mapbox/mapbox-maps-ios/issues/2233&lt;/span&gt;
        &lt;span class="c1"&gt;# Once this issue is fixed we can upgrade to 16&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install XCode&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;maxim-lobanov/setup-xcode@vx.x.x&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;xcode-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15.4&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Ruby&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@vx.x.x&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.3.0&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Bundler&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem install bundler&lt;/span&gt;
        &lt;span class="c1"&gt;# TODO: Cache node_modules similarly to how Pods are cached (compare hash of yarn.lock)&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Yarn Install&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile/${{ inputs.app }}&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn install --immutable&lt;/span&gt;
        &lt;span class="c1"&gt;# TODO: Cache Gems similarly to how Pods are cached (compare hash of Gemfile.lock)&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Gems&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle install&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile/${{ inputs.app }}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache CocoaPods dependencies&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@vx&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;FILES_GLOB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile/${{ inputs.app }}/ios/Podfile.lock&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;mobile/${{ inputs.app }}/ios/Pods&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-pods-${{ hashFiles(env.FILES_GLOB) }}&lt;/span&gt;
            &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;${{ runner.os }}-pods-&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Pods&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile/${{ inputs.app }}/ios&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec pod install&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build iOS App&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;APP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.app }}&lt;/span&gt;
            &lt;span class="na"&gt;SCHEME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.ios-scheme }}&lt;/span&gt;  &lt;span class="c1"&gt;# otherCompanyFirstAppProduction, myCompanySecondAppTest&lt;/span&gt;
            &lt;span class="na"&gt;BRAND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.inferred-brand }}&lt;/span&gt;
            &lt;span class="na"&gt;TARGET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.prep-env-variables.outputs.ios-target }}&lt;/span&gt;
          &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec fastlane gym&lt;/span&gt;
          &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile/${{ inputs.app }}&lt;/span&gt;
        &lt;span class="c1"&gt;# Upload&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload application&lt;/span&gt;
          &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@vx&lt;/span&gt;
          &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{steps.prep-env-variables.outputs.ios-scheme}}&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mobile/${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/ios/build/${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;steps.prep-env-variables.outputs.ios-scheme&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.ipa"&lt;/span&gt;
            &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
            &lt;span class="na"&gt;overwrite&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="na"&gt;if-no-files-found&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 5 - Troubleshooting
&lt;/h3&gt;

&lt;p&gt;More than likely these won't work the first time. Time to go back and adjust. Note that since the workflow is now in the main branch you can test your workflow changes on a feature branch. Just select your feature branch in the "Branch" dropdown shown above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Things to Note
&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;App Center gives you the ability to write `appcenter-pre-build.sh` and `appcenter-post-build.sh` scripts. The `env-file-prep.sh` is basically that same thing, just without the context of appcenter.&lt;/li&gt;
  &lt;li&gt;One of our apps is using MapBox which needs a `.netrc` in the root directory. If you need something similar, you can add a step to your action by adding the code shown in the the "optional mapbox" part + the `./my-first-app/prep-mapbox.sh` in the Appendix section below.&lt;/li&gt;
  &lt;li&gt;After our work on this was done (and only as I'm writing this article) we realized we can use the `sparse-checkout` option from `actions/checkout` to only check out the needed files. This should speed up our workflow runtime even more!&lt;/li&gt;
  &lt;li&gt;It may be helpful for you to add the build and/or version number onto the artifact. We haven't done that but it's something we're interested in adding for the future.&lt;/li&gt;
  &lt;li&gt;Troubleshooting certificates for iOS was a HUGE pain in the butt. I don't have any good advice here other than to realize (and communicate with your POs) that this part may take a while.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It regularly took over 50 minutes for our mobile app to build in App Center. Part of that could have very likely be improved by adjusting App Center configurations &amp;amp; how we store and bundle app assets but after migrating our builds to GitHub Actions our app build times are now down to 22 minutes - More than twice as fast!&lt;/li&gt;
&lt;li&gt;All the rest of the software at Vandebron (backend services in Scala and Python and frontend applications in Typescript + React) is built using GitHub Actions. This move brings mobile apps in line with all other software. This move to GHA for mobile builds has led to several of our mobile devs getting our hands dirty in GHA, which is great because we can now play a role in the larger CICD discussions.&lt;/li&gt;
&lt;li&gt;We have full control over our CICD pipeline for mobile builds. In the future we can integrate more Fastlane commands to further automate the release process.&lt;/li&gt;
&lt;li&gt;We did a full ADR (shown below) which initiated the work here. Links referenced in image are in Appendix below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fctbdwyhh78xvu85nu22p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fctbdwyhh78xvu85nu22p.png" alt="Architecture Decision Record for why Vandebron chose GitHub Actions for our Mobile CICD Build Pipeline" width="800" height="1295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mapbox Integration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;file: .github/workflows/mobile-apps-build-android.yaml and .github/workflows/mobile-apps-build-ios.yaml (optional mapbox)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Mapbox .netrc file (my-first-app only)&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.app == 'my-first-app' }}&lt;/span&gt;
    &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mobile&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MAPBOX_READ_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.MAPBOX_READ_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash ./my-first-app/prep-mapbox.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;file: ./my-first-app/prep-mapbox.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

  &lt;span class="c"&gt;# WARN: DO NOT use tee here (it prints to console)&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"machine api.mapbox.com"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"login mapbox"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"password &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MAPBOX_READ_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.netrc
  &lt;span class="nb"&gt;chmod &lt;/span&gt;0600 ~/.netrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ADR Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions" rel="noopener noreferrer"&gt;GHA (Billing)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bitrise.io/" rel="noopener noreferrer"&gt;Bitrise&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codemagic.io/" rel="noopener noreferrer"&gt;CodeMagic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=_uRbEyBa9q0" rel="noopener noreferrer"&gt;YouTube video showing GHA setup for Android&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/scaleuptech/how-to-make-react-native-builds-with-github-actions-8d0203801eff" rel="noopener noreferrer"&gt;Medium article showing GHA setup for Android&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.obytes.com/blog/react-native-ci-cd-github-action" rel="noopener noreferrer"&gt;Article discussing how build uploads can be done&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>githubactions</category>
      <category>gha</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Choosing Remix as a Server-Side Rendering (SSR) Framework</title>
      <dc:creator>John Fisher</dc:creator>
      <pubDate>Thu, 31 Oct 2024 11:29:50 +0000</pubDate>
      <link>https://dev.to/vandebron/choosing-remix-as-a-server-side-rendering-ssr-framework-1g08</link>
      <guid>https://dev.to/vandebron/choosing-remix-as-a-server-side-rendering-ssr-framework-1g08</guid>
      <description>&lt;h2&gt;
  
  
  The Background
&lt;/h2&gt;

&lt;p&gt;We at Vandebron have a mission to get the news out about &lt;a href="https://vandebron.nl/missie" rel="noopener noreferrer"&gt;our good work&lt;/a&gt;, and we understand that &lt;a href="https://web.dev/articles/rendering-on-the-web#server-side" rel="noopener noreferrer"&gt;Server Side Rendering (SSR)&lt;/a&gt; can really help with that. Among other things, it provides an easy way for search engines to discover our pages, so you, our (future?!) customer, can find them more easily. That means more people choosing green energy, and ultimately, a cleaner environment!  🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  We rolled our own
&lt;/h2&gt;

&lt;p&gt;The year was 2017, Covid was still a word that sounded more like a bird than anything else... The world was heating up and Vandebron was 4 years into its mission to bring 100% renewable energy throughout all of the Netherlands.&lt;/p&gt;

&lt;p&gt;As far as web technologies are concerned, 4 years was ages ago. It was a time when NextJS was less than a year old, and Remix was still several years from coming out. But we needed a way to deliver that high-quality content to all of you. So, the innovators that we were, we decided to build our own SSR framework. In short, we wanted pizza, but there were no pizza shops in town... So we made our own!&lt;/p&gt;

&lt;p&gt;It's been great but not without issue...&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;tr&gt;
    &lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Funujw4l5pqf1v7k7xa0h.png" alt="ugly-window-mock" width="800" height="233"&gt;&lt;/td&gt;
    &lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjv2q6mgezcdhryyga8nm.png" alt="remix-migration-mocking-a-window" width="800" height="1094"&gt;&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A Short Note: Why Server Side Rendering
&lt;/h2&gt;

&lt;p&gt;You might not be satisfied with the short explanation of why we picked an SSR framework in the first place. This article isn't really about that - if you're interested in more analysis on when and where to choose an SSR framework, check out these excellent articles from Splunk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.splunk.com/en_us/blog/learn/server-side-rendering-ssr.html" rel="noopener noreferrer"&gt;The User Experience (UX) Benefits of SSR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.splunk.com/en_us/blog/learn/server-side-rendering-ssr.html" rel="noopener noreferrer"&gt;The SEO Benefits of SSR&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decisions Made the Right Way - A Comparison
&lt;/h2&gt;

&lt;p&gt;Nowadays, there are better, industry standard technologies available! I.e. pizza shops have opened nearby!! Let's find a good one. Of course, you don't want to just go to any spot. Especially if there's more than one shop in town - you'd be silly not to check which one is closest, and look at the menu. Which one has better reviews, is that one very angry customer just upset that there wasn't any anchovies in the vegan pizza shop? What were they expecting anyway?&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwj1jj9chd296wm3f6zeq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwj1jj9chd296wm3f6zeq.png" alt="Vegan pizza shop" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At Vandebron we're a React shop, so we limited ourselves to just SSR frameworks supporting React. The choice of one framework over another is of crucial importance, so, as part of our analysis, we built a small part of our &lt;a href="https://vandebron.nl/blog" rel="noopener noreferrer"&gt;vandebron.nl/blog&lt;/a&gt; page twice. Two of our engineers then presented these prototypes to our Front End Guild, and this discussion fed heavily into the Architecture Decision Record that we wrote comparing the results.&lt;/p&gt;

&lt;p&gt;* At Vandebron, Guilds are groups of engineers from disparate teams that are interested in a single domain: i.e. Backend, Frontend, IAM and Auth, etc. &lt;/p&gt;

&lt;p&gt;The Background for the decision record states this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Our Frontend currently uses a custom-built, hard to maintain SSR solution, which we'd like to replace with a modern and standard library. Possible candidates are NextJS and Remix. The goal is to investigate which one suits our needs best."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes, there are other options we could have considered but we wanted to stay with a tried-and-tested framework and one that was compatible with our existing React setup.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9h66hnq45wxucb006v0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9h66hnq45wxucb006v0.png" alt="ADR pros and cons between Remix and NextJS" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the comparison between the two frameworks was very similar. In the end we favoured the simple, opinionated direction of Remix over that of the more full-featured but potentially complex setup of NextJS. Even though Remix has a smaller community, we attributed this mostly to the age of the framework and not the quality  of the framework itself. Though the Positivity has gone down a bit (as listed in &lt;a href="https://2023.stateofjs.com/en-US/libraries/meta-frameworks/" rel="noopener noreferrer"&gt;the 2023 StateOfJS survey&lt;/a&gt;,) the decrease has been relatively minor and in line with most-other frameworks (notable exceptions for Astro and SvelteKit which have both seen big upticks in both Usage and Positivity)&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtr9a6eebs8grd8r8ox0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtr9a6eebs8grd8r8ox0.png" alt="State of JS framework positivity" width="800" height="468"&gt;&lt;/a&gt;&lt;br&gt;
Finally, we noted that NextJS is tightly coupled with Vercel. At Vandebron we value platform independence and not getting tied to specific hosting providers or platforms. Remix gives us the independence we're looking for by providing a SSR framework without a potential to be tied into other solutions/platforms in the future.&lt;/p&gt;

&lt;p&gt;Outcome&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Most members favoured Remix’s focus on web standards and usage of standard libraries and were put off (a little) by NextJS’s uncertainty in development direction."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  So, How's it Going?
&lt;/h2&gt;

&lt;p&gt;The migration effort is still underway but already we can report that it's going quite well - developers are excited to work on the new tech stack because it's seen as a developer-friendly platform and one of the two leading frameworks in the industry. In the words of one engineer: "Dev experience has improved massively, it's fun, it's easy to work with"&lt;br&gt;
Here are some of the things we still need to work on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Our Docker image is quite large as it includes all the &lt;code&gt;node_modules&lt;/code&gt;. We think we can clean this up a bit by using Yarn's Plug'n'Play (PnP) feature which should lead to faster image-build times and faster container startup times.&lt;/li&gt;
&lt;li&gt;With our custom SSR solution, we use Redux Toolkit (RTK) and RTKQuery on the server... This is of course an anti pattern on the server, since server-side logic should be stateless. The Remix framework does already tries to be smart with it's loaders, so the benefits we might have gotten from RTK aren't needed there.&lt;/li&gt;
&lt;li&gt;We feel the application we're migrating from is doing too much - it includes our marketing pages like the &lt;em&gt;Blog&lt;/em&gt; and &lt;em&gt;Mission&lt;/em&gt; pages we've been working on for the initial release, as well as the pages for our our signup and renewal process (become a Vandebron customer &lt;a href="https://vandebron.nl" rel="noopener noreferrer"&gt;here&lt;/a&gt;!!!) This is a separate conversation, and ultimately one for the FE Guild, but the existing app's size and purpose is making the migration take longer than it should, and forcing us to put some routing rules in place to make sure the right parts of our old site are getting swapped out for the new.&lt;/li&gt;
&lt;li&gt;Previously, many of the images and PDFs we used on our website were checked directly into the repo. Part of our migration to Remix made us realize we should be using a CMS for this. We are already integrated with a CMS, we just need to be making better use of it in some cases.&lt;/li&gt;
&lt;li&gt;We haven't explored the Remix-specific linting rules yet. While we're confident in the existing React and TS lint rules we already have, it seems like configs like &lt;a href="https://www.npmjs.com/package/@remix-run/eslint-config" rel="noopener noreferrer"&gt;@remix-run/eslint-config&lt;/a&gt; could be quite handy.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>remix</category>
      <category>ssr</category>
      <category>typescript</category>
      <category>react</category>
    </item>
    <item>
      <title>Creating a Self-Service Data Model</title>
      <dc:creator>Mats Stijlaart</dc:creator>
      <pubDate>Tue, 06 Feb 2024 10:42:38 +0000</pubDate>
      <link>https://dev.to/vandebron/creating-a-self-service-data-model-1mm7</link>
      <guid>https://dev.to/vandebron/creating-a-self-service-data-model-1mm7</guid>
      <description>&lt;p&gt;The title of this article could have also been "&lt;em&gt;Getting Rid of an Unmanageable Legacy Data Model&lt;/em&gt;", but after a year-long migration project the current title does more justice to the progress made. &lt;/p&gt;

&lt;h4&gt;
  
  
  Compiled Legacy as a Data Model
&lt;/h4&gt;

&lt;p&gt;Our former data model was a series of parallel custom python jobs all covering every step of the &lt;em&gt;Extract-Transform-Load&lt;/em&gt; (ETL) process from sources into report. Specific transformation got performed a numerous amount of times in multiple different jobs, daily. This made us prone to bugs, slow on development and maxing out on compute. &lt;/p&gt;

&lt;p&gt;The situation became so pressing that keeping alive simple reporting to the business became a daily burden on the Data Analytics team, limiting resources for advanced analytics and leveraging data sources for competitive insights.&lt;/p&gt;

&lt;p&gt;We concluded the old set-up to be outdated and looked around for current best practices concerning data infrastructure. Trying not to reinvent the wheel and staying away from designing custom solutions that had bitten us in the past, we decided to adopt a combination of &lt;em&gt;Snowflake&lt;/em&gt;, &lt;em&gt;dbt&lt;/em&gt; and &lt;em&gt;Lightdash&lt;/em&gt; to start forming a new data landscape.&lt;/p&gt;

&lt;p&gt;This revamping of the set-up gave us the opportunity to start over, using the power of &lt;em&gt;dbt&lt;/em&gt; to create a modular data model where you could leverage different stages of data, while creating shared definitions, a single source of truth and documentation.&lt;/p&gt;

&lt;h4&gt;
  
  
  What We Came Up With?
&lt;/h4&gt;

&lt;p&gt;We went for a pretty classic &lt;em&gt;dbt&lt;/em&gt; data model design, introducing 5 layers of data: staging, entity, intermediate, mart and api. Each layer serving a specific purpose.&lt;/p&gt;

&lt;h5&gt;
  
  
  Staging
&lt;/h5&gt;

&lt;p&gt;With all data coming in from different sources, this is where we ensure the data all adheres to the same conventions and formatting. This introduces a nice developer experience for the next layers, by introducing consistency across different sources. It also serves as the go to place for advanced or deep dive analysis that do not get answered by the downstream layers, which could potentially spark data modelling developments.&lt;/p&gt;

&lt;h5&gt;
  
  
  Entity
&lt;/h5&gt;

&lt;p&gt;After uniforming the data, we create entities that form the building blocks of the downstream layers and analyses of our business analysts. We built entities along the core aspects of our product, capturing shared definitions in data and bringing together relevant features using the &lt;em&gt;One-Big-Table&lt;/em&gt; (OBT) principle. We try to refrain from long queries or extensive use of CTE's, resulting in simplistic models. These models serve our business analysts by reducing the complexity of their queries with all joins and filters taken care of, denormalizing the database structure. This has shifted the place where ad-hoc BI requests are fulfilled from the central data team to the domain business teams, applying principles of a data mesh.&lt;/p&gt;

&lt;h5&gt;
  
  
  Intermediate
&lt;/h5&gt;

&lt;p&gt;With some models rising in complexity and computation, we use the intermediate layer to split this complexity and computation across multiple models. These intermediate models are rarely queried because they serve no reporting or analytical purpose. Think of incremental date spine explosions or highly complex business logic broken down into multiple models.&lt;/p&gt;

&lt;h5&gt;
  
  
  Mart
&lt;/h5&gt;

&lt;p&gt;This is the main layer where we provide self-service to less technical employees within the organization, creating ready-made tables. We aggregate along date spines and dimensions to create readable models. It is where we leverage &lt;em&gt;Lightdash&lt;/em&gt; metrics to create dynamic tables to provide business teams with a certain amount of freedom in terms of the granularity and dimensions they want to report on in their dashboarding. The use of entities as building blocks has aligned reporting across domain business teams, creating a single and centralized source of truth and relieving the data team from explaining distinctions. So while the dimensions can be tweaked for specific use cases, the definitions of the metrics are set in code.&lt;/p&gt;

&lt;h5&gt;
  
  
  API
&lt;/h5&gt;

&lt;p&gt;With some dependencies outside of the data model, we use an API layer on top of our mart to record exposures towards different services and provide views which explicitly contain only the necessary datapoints.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcv94fcrvw0cmhy5ms41z.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcv94fcrvw0cmhy5ms41z.jpg" alt="A schematic overview of the data model structure" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The Process
&lt;/h4&gt;

&lt;p&gt;We decided to take advantage of the chaos created by the old situation: no real single source of truth gave us the opportunity to create a truth. Investigating business teams' needs, we created data definitions in entities. We kept a pragmatic approach to these definitions, being flexible towards business teams' specific needs but also limiting the allowed complexity or number of exceptions. The new data model should answer everyone's questions, but should also be understood by everyone.&lt;/p&gt;

&lt;p&gt;We forced ourselves to have descriptions for all data from the entity layer onwards, because only defining and describing the entities in code is not enough. We leveraged the embedded business analysts' knowledge to form the descriptions, noting that the best description is the one the user understands (because they wrote it).&lt;/p&gt;

&lt;p&gt;With the ready-made marts in place, we decided to give away most of the dashboarding responsibility to the business teams. The embedded analysts are very apt at defining and designing their relevant insights into dashboards. The central data team only took ownership of company wide dashboards and provided support on the dashboarding where necessary.&lt;/p&gt;

&lt;p&gt;After the adoption of the new stack, we noticed that the more technical embedded analysts were very interested in learning a new tool and language. So, we started a data model group and onboarded multiple embedded business analysts as data model developers. This has massively increased the speed of development of the data model. Primarily, because of specific business domain knowledge not needed to be transferred to the developers in the central data team first, but the knowledge holders developed models themselves. The central data team took on a different role: providing infrastructural support, improving on efficiency, monitoring costs and creating a vision and strategy for organic but structured growth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ne5xarz6pk13jaz6a6a.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ne5xarz6pk13jaz6a6a.jpg" alt="A schematic overview of the final self-servicing data model product" width="800" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  What Did We Learn?
&lt;/h4&gt;

&lt;p&gt;Few key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some business teams have more requirements in terms of definitions than other teams, so if other teams allow, be pragmatic and just go for the stricter requirements.&lt;/li&gt;
&lt;li&gt;Enabling self-service analysis means giving away control, take this into account in your data descriptions. They should be clear and concise.&lt;/li&gt;
&lt;li&gt;Educate users on the designed structure of the data model, explain what layer serves which purpose and what questions can be answered how and where.&lt;/li&gt;
&lt;li&gt;Create clear communication and support channels for the business to kickstart the adoption, you are not the only one learning a new tool.&lt;/li&gt;
&lt;li&gt;Data is not only for the data team, so encourage those passionate and enthusiastic analysts to co-create! (Just keep an eye on the project.) &lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dbt</category>
      <category>snowflake</category>
      <category>datamodel</category>
      <category>selfservice</category>
    </item>
    <item>
      <title>So, back to the monolith it is then?</title>
      <dc:creator>Sam Theisens</dc:creator>
      <pubDate>Tue, 23 May 2023 06:37:33 +0000</pubDate>
      <link>https://dev.to/vandebron/so-back-to-the-monolith-it-is-then-1p98</link>
      <guid>https://dev.to/vandebron/so-back-to-the-monolith-it-is-then-1p98</guid>
      <description>&lt;h3&gt;
  
  
  Amazon embraces the mighty monolith
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XSns7-s3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sc1yucw2h9thbhzgnwoi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XSns7-s3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sc1yucw2h9thbhzgnwoi.png" alt="image alt text" width="800" height="635"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In March 2023, Amazon published a &lt;a href="https://www.primevideotech.com/video-streaming/scaling-up-the-prime-video-audio-video-monitoring-service-and-reducing-costs-by-90"&gt;blog post&lt;/a&gt; detailing how they had managed to reduce the cost of their audio-video monitoring service by 90%.&lt;br&gt;
The &lt;em&gt;key&lt;/em&gt; to this reduction was migrating from a distributed, microservice architecture to a &lt;em&gt;monolith&lt;/em&gt;.&lt;br&gt;
The blog post went viral, prompting some software industry celebrities to &lt;br&gt;
&lt;a href="https://world.hey.com/dhh/even-amazon-can-t-make-sense-of-serverless-or-microservices-59625580"&gt;question&lt;/a&gt; the entire concept of microservices.&lt;/p&gt;
&lt;h3&gt;
  
  
  What should we learn from this?
&lt;/h3&gt;

&lt;p&gt;So, does this mean microservices are fundamentally flawed? Should we all migrate back to monoliths?&lt;br&gt;
&lt;em&gt;No&lt;/em&gt; and &lt;em&gt;definitely no&lt;/em&gt; I would say. Instead, my takeaways from this article are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Microservices aren't about scaling for performance.&lt;/strong&gt; At least not primarily. Although horizontally scalability for computationally intensive operations &lt;em&gt;can&lt;/em&gt; be very useful or even essential in some cases, it tends to be a rare benefit. Very often, performance bottlenecks are IO bound and caused by external systems beyond your control. Nevertheless, there &lt;em&gt;are&lt;/em&gt; other compelling reasons to consider microservices: they &lt;em&gt;force&lt;/em&gt; you to communicate via contracts, &lt;em&gt;encourage&lt;/em&gt; you to organize your functionality around domains, and &lt;em&gt;allow&lt;/em&gt; you to scale your organization. Of course, all this comes at considerable costs. There's no free lunch 👇.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;Don't underestimate the power of a single CPU in 2023&lt;/b&gt;. 
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/RC_FHNRI8Lg"&gt;
&lt;/iframe&gt;
 To judge whether a process is unreasonably slow or not, I tend to think of the fact that already in the 1990s, screens showed 65K pixels at any given time. Back then, multiple arithmetic calculations (additions, subtractions) could be performed for each pixel, fifty times per second. Nowadays, your screen probably displays more than 5 Million pixels at once. So, if the amount of datapoints you are dealing with in the order of millions, you should generally be able to process them in a matter of seconds on a single machine. If you can't, you may be doing something very inefficient.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Software engineering is hard&lt;/strong&gt;. Mistakes are made all the time, everywhere. Even at the big 4 tech companies. Kudos to Amazon 👏 for openly sharing the mistake they made so that we may all learn.
In the next section I will share one of our own experiences, not entirely different from the Amazon example.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  The 90% cost reduction case at Vandebron
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Microservices or just distributed computing?
&lt;/h4&gt;

&lt;p&gt;Considering that all the functionality used in the Amazon case belongs to the same &lt;em&gt;domain&lt;/em&gt;, it arguably does not even serve as &lt;br&gt;
a case against improper use of microservices, but instead a case against misuse &lt;em&gt;distributed computing&lt;/em&gt;. &lt;br&gt;&lt;br&gt;
Let's look into an example of misuse of distributed computing at Vandebron now.&lt;/p&gt;
&lt;h4&gt;
  
  
  Predicting the production of electricity
&lt;/h4&gt;

&lt;p&gt;For utility companies, accurately predicting both electricity consumption and production is crucial.&lt;br&gt;
Failing to do so can result in blackouts or overproduction, both of which are &lt;a href="https://vandebron.nl/blog/hoe-houdt-onze-technologie-het-energienet-in-balans"&gt;very costly&lt;/a&gt;.&lt;br&gt;
Vandebron is a unique utility company in that the electricity that our customers consume is produced by a &lt;a href="https://vandebron.nl/energiebronnen"&gt;very large&lt;br&gt;
amount&lt;/a&gt; of relatively small scale producers, who produce electricity using windmills or solar panels.&lt;br&gt;
The large number and the weather dependent nature of these producers make it very hard to predict electricity generation accurately.&lt;/p&gt;

&lt;p&gt;To do this, we use a machine learning model that is trained on historical production data &lt;br&gt;
and predictions from the national weather &lt;a href="https://www.knmi.nl/"&gt;institute&lt;/a&gt;. As you can imagine, this is a computationally intensive task, involving large amounts of data.&lt;br&gt;
Fortunately, we have &lt;a href="https://www.vandebron.tech/blog/fueling-the-energy-transition-with-spark-part-1"&gt;tooling in place&lt;/a&gt; that&lt;br&gt;
allows us to distribute computations of a cluster of machines if the task is too large for a single machine to handle.&lt;/p&gt;

&lt;p&gt;However, here's the catch: the fact that we &lt;em&gt;can&lt;/em&gt; distribute computations does not mean that we should. Initially it seemed that&lt;br&gt;
we couldn't analyze the weather data quick enough for the estimation of our production to still be a &lt;em&gt;prediction&lt;/em&gt;&lt;br&gt;
rather than a &lt;em&gt;postdiction&lt;/em&gt;. We decided to distribute the computation of the weather data over a cluster of machines.&lt;br&gt;
This worked, but it made our software more complex and Jeff Bezos even richer than he already was.&lt;/p&gt;

&lt;p&gt;Upon closer inspection, we found an extreme inefficiency in our code. It turned out that we were repeatedly reading the entire weather dataset&lt;br&gt;
into memory, for &lt;em&gt;every&lt;/em&gt; single "pixel". After removing this performance bug, the entire analysis could &lt;em&gt;easily&lt;/em&gt; be done&lt;br&gt;
on a single machine. &lt;/p&gt;
&lt;h3&gt;
  
  
  What more is there to say? &lt;a id="presentation"&gt; &lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;So if microservices aren't about performance, what &lt;em&gt;are&lt;/em&gt; they about? If I had to sum it up in one sentence It would be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Microservices are a way to scale your organization&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is a lot of detail hiding in that sentence, which I can't unpack in the scope of this article. If you're interested&lt;br&gt;
what microservices have meant for us, I would recommend you watch the presentation below.&lt;/p&gt;
&lt;h4&gt;
  
  
  Microservices at Vandebron
&lt;/h4&gt;

&lt;p&gt;At &lt;a href="https://vandebron.nl/"&gt;Vandebron&lt;/a&gt;, we jumped onto the "microservice bandwagon" circa 2019. This wasn't a decision&lt;br&gt;
made on a whim. We had seen a few industry trends come and go, so we first &lt;a href="https://samnewman.io/books/building_microservices_2nd_edition/"&gt;read up&lt;/a&gt;&lt;br&gt;
and did our own analysis. We found that the concept of microservices held promise, but also knew that they would come at a cost.&lt;/p&gt;

&lt;p&gt;These are some of the dangers we identified and what we did to mitigate them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Danger&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;A stagnating architecture&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Compile and unit-test time detection of breaking changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Complicated and error prone deployments&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Modular CI/CD &lt;a href="https://github.com/Vandebron/mpyl"&gt;pipelines&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Team siloization&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;A single repository (AKA monorepo) for all microservices and a discussion platform for cross-domain and cross-team concerns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Duplication of code&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Shared in house libraries for common functionality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The following presentation to the students of &lt;a href="https://vu.nl/"&gt;VU University, Amsterdam&lt;/a&gt; explains how we implemented&lt;br&gt;
some of these mitigations and what we learned from them.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/HDs-pCsEzKM"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>architecture</category>
      <category>aws</category>
    </item>
    <item>
      <title>The difference between a component library and a design system</title>
      <dc:creator>Petter Andersson</dc:creator>
      <pubDate>Mon, 08 May 2023 16:26:14 +0000</pubDate>
      <link>https://dev.to/vandebron/the-difference-between-a-component-library-and-a-design-system-2of0</link>
      <guid>https://dev.to/vandebron/the-difference-between-a-component-library-and-a-design-system-2of0</guid>
      <description>&lt;p&gt;&lt;strong&gt;A while back, our former technology manager Roy Derks covered the subject of component libraries here on the blog. From a technical perspective, he spoke about when you need one (and when you don’t need one) and what to consider when building one. Since then, obviously a lot has happened at Vandebron. But one of the more interesting things to happen is that design became an integrated part of the digital department, as opposed to previously being attached to marketing. In this new setup, one of the first major projects the design team was involved in was the alignment of our component libraries. And no that’s not a typo, that’s libraries as in the plural form of library. Confusing? I thought so too. In this blog I’ll try to explain further why that was the case, how the work actually helped us bridge the gap between design and development, and dissect the work of unifying those component libraries into one single source of truth and ultimately what’s to become our design system.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A bit of a mess
&lt;/h3&gt;

&lt;p&gt;Before we get into it, some background as to where we started out might be useful. As previously mentioned, the design team had just become a part of the digital department and one of the first tasks at hand was the creation of a design system. In the design team, we had previously worked with a certain set of brand guidelines, a style guide if you will, which had not necessarily been translated or aligned to the requirements of a digital product or development environment. Development had also created a set of stylesheets and libraries with reusable components which they used to reduce development time. Having it all separately might sound a bit counter-intuitive, but not very surprising if you consider designers and developers not being in the same department, working on a different timeline, different priorities and so forth. However, this only highlighted the importance of designers and developers working together and the need for a proper design system to help prevent creating a fence between the teams causing unnecessary and ineffective work on both sides. The result of this previous “unsynciness”, a rebrand in 2017, and a re-aligned techstack, was the existence of 3 different libraries and subsequently 3 different sources of truth within the development environment. To add to this, we also had separate design guidelines geared more towards brand/marketing purposes in the design team. Now came the rather massive task of unifying these and eventually, rather than having just a library, &lt;em&gt;having a system&lt;/em&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Component library ≠ design system
&lt;/h3&gt;

&lt;p&gt;Now, there’s a lot of terminology here that might be difficult to grasp if you’re new to the subject. So I thought I’d clarify what we mean when referring to these, how they fit into the context of our situation, and how many of them we had!&lt;/p&gt;

&lt;h4&gt;
  
  
  - Brand guidelines / style guide (design)
&lt;/h4&gt;

&lt;p&gt;A set of guidelines and examples outlining all the visual elements of a brand such as logos, color, typography, imagery etc. and subsequently in what - - manner they should be applied. It can also be expanded to include more things brand related such as tone of voice, brand values and so forth. Often with brand guidelines, they are created from a marketing perspective and the digital experience(or product) aspect of how the brand should be applied/represented is usually thought about in the second hand, or not included at all. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amount: 1&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  - Design kit/library (design)
&lt;/h4&gt;

&lt;p&gt;A designer resource file with all the available building blocks that make up the digital design language of a brand and/or the user interface of a product. This is usually only visual(no code) and lives in the design software of the designer's choosing. For us this used to be Sketch, but we recently moved to Figma. Can also include documentation and examples of how the different building blocks should be applied and utilized. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amount: 1&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  - Style sheet (front-end)
&lt;/h4&gt;

&lt;p&gt;A set of styling properties to be applied when rendering a web page, usually in the format of CSS. This can include things related to the brand guidelines such as font size, colors, etc. but also things related to web layout such as the margins and paddings of different web elements.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amount: 1&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  - Component library (front-end)
&lt;/h4&gt;

&lt;p&gt;A set of dynamic web components that can be used in a development environment in order to quickly build user interfaces. This helps to ensure consistency, to avoid rebuilding the same component more than once and to avoid changing said component in more places than one, and subsequently help reduce development time. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amount: 3&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;All of the above mentioned things, together with rigorous documentation, amount to what’s called a design system. Having it all combined in a structured way is key to getting the most out of such a system. In our case, most of these things were separate and not necessarily connected to each other. But what stands out most of the things above is probably the fact that we, over time, had amounted to 3 different component libraries. I mentioned earlier how that scenario had transpired so I won’t go into too much detail as to how that happened, but if you’re a developer in a small to medium-sized company and I mention “rebrand” and “new techstack” you can probably figure out how. However complex, this also proved to be an excellent opportunity for our developers and for us in the design team. We finally get to unify our component libraries into one, while simultaneously aligning it with our design kit and expanding the guidelines with new and updated documentation. Thus ensuring that designers and developers speak the same language and share the same single source of truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  A guild forms
&lt;/h3&gt;

&lt;p&gt;To kickstart this process we formed a project group(or ‘guild’) composed of 2 designers and 2 developers, each designer and developer from the two consumer-facing scrum teams. The idea was to let the developers work on the migration and unification of the component libraries in collaboration with us designers in the same project, making it easier to align and to create co-ownership of the product. Our first step was to decide on the structure of our component library, this way the developers could slot all the existing, reworked and new components into the right place in the new library. Easy enough right? Well, here comes our first challenge. We initially wanted to take an atomic approach and build our components from the well known and widely used atomic design principles. We also needed to consider the 3 different “product groups” which the library should apply to, all still utilizing the same style properties. &lt;/p&gt;

&lt;p&gt;Vandebron has a wide range of products serving different platforms, with the visual language remaining the same but where the user interface might differ. This requires the top elements of the system(such as colors and typography) to be shared across all products, whereas the lower you get the more product-specific an element becomes. This is the reason why we wanted to structure the system according to the principles of Atomic Design first, in order to assign the components to a hierarchical structure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--uD3hhldg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a2w6w8mno58n5rlir98l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uD3hhldg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a2w6w8mno58n5rlir98l.jpg" alt="Atomic Design by Brad Frost" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this approach the atoms would work like design tokens and the molecules would be components general enough that they’d be shared across all product groups, this CORE part of the library would essentially be the stylesheet that impacts all visual aspects of the digital brand experience. Only on organism-level do we start to differentiate what product group the component belongs to. So a change to the CORE parts of the library(atoms or molecules) would impact all components in all product groups.&lt;/p&gt;

&lt;p&gt;However, this approach actually made less sense from a development perspective. Not that it wouldn’t work or that the categorization didn’t make sense, but it would require us rewriting all the already existing components. Components that are actively in use. We deemed this approach a bit too high-risk and high-effort for the time being and started looking into alternatives, while still keeping the atomic structure as a more long-term goal. Another thing our initial idea didn’t take into account was the experience of the future main user of the library, &lt;strong&gt;&lt;em&gt;the developer!&lt;/em&gt;&lt;/strong&gt; Organizing a design system after the brand properties and product groups makes a lot of sense from a designers or a marketeers perspective, and it should probably still be presented outwards that way, but a component library is something else(remember?). So based on our development environment and the way we build our websites and apps our developers suggested a different structure:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qVeA0zso--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mk16ikcz8hkuzgz6cruo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qVeA0zso--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mk16ikcz8hkuzgz6cruo.jpg" alt="Iteration on structure" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this structure, similar to the previous one, the components are instead categorized and sorted by how they should be applied to the page or application that’s being built. Styles, layouts and inputs are general enough to be applied to all product groups whereas from the surface level the components start becoming more specific in their use case. That way, the components can be separated into specific or even several product groups. In this format the components themselves are not as intertwined as in the atomic structure, albeit still connected by the style element. So while it’s a bit more resistant to overall changes the main idea of having the same style properties applying to everything still works, and it helps us designers to better relate and contextualize what we’re designing from more of a development perspective, thus helping bridge the gap between development and design even further. The main insight we drew from this experience is to not let industry standards and certain trends dictate what you should do. Sure they’re important to keep an eye on, but do it with carefulness and always apply an asterisk to it. Figure out what works best for your specific situation and what’s realistic in the short-term vs. in the long-term. There’s no one-size-fits-all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speaking the same language
&lt;/h3&gt;

&lt;p&gt;With the component library migration now underway, we started looking into ways to improve our system from the designers' side of things. As previously mentioned, we had just gone from using Sketch to using Figma and with that came a good opportunity to rebuild, adjust and expand our design kit also. We did that by removing, adding, simplifying and renaming a lot of what was in there since before and with the design kit now adjusted to match the component library we were now also speaking the same language. We can actually now compare this side-by-side with the tools we’re using. In Storybook we have attached the Figma design of every component, simply by activating the feature and pasting the link to its page or artboard in the Figma file. This will refresh in almost real-time if any changes are made so we can easily spot any differences and inconsistencies between what’s live and how the design looks. In Figma, we try to document all our components and give some context as to how it works and should be applied. This is now also directly visible to the developer in the context of the component library. Expanding on our documentation and exposing our digital design guidelines like that has been a great way to create a shared understanding of our designs. Rather than just an image being tossed over a fence, there is now quite literally a direct connection between design and development and therefore also more of a shared ownership.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kazgq92f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7wuqpze65ndvmncn47in.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kazgq92f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7wuqpze65ndvmncn47in.jpg" alt="Storybook Figma integration" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Further defining the process
&lt;/h3&gt;

&lt;p&gt;As all the alignment on the design side and the migration neared completion, we started seeing a lot of things that could be improved upon or even added to our component library. When we started logging these things down on our project backlog we quickly realized that the scope of our project had quickly been growing into something beyond what was initially intended, and that rather than giving us focus this guild format was instead at risk of creating an isolated bubble of knowledge surrounding the design system. This prompted us to gauge the opportunity and capacity among our development teams to instead tackle these tasks together, either alongside or within their respective day-to-day tasks. In order to do so we needed the buy-in from key stakeholders such as the product owners from the respective development teams. It’s obviously a big ask to get 1 developer from each team to work on improving a component library, especially when they’ve already given us a quarter on migration and have other important business and/or user needs to tend to. So instead, we looked into how we can embed the improvement and further development of our design system into the developers current processes and primary day-to-day work. We structure this by linking our component library improvement/addition tickets to relevant tickets in their respective sprints. In defining the workflow like this, our 2 designer 2 developer guild in effect rendered unnecessary and instead we opened up the co-ownership and contribution to all developers in all customer-facing development teams and in the process of it preventing isolating knowledge too much. In opening up the process like this, another positive side effect we see is the involvement, engagement and subsequent use of our component library going up. With the product designers now also actively a part of the front-end guild meetings, we have an ever bigger forum and bigger opportunity to build a world class component library and design system while also having more hands on deck to work on maintenance and improvements. We still have a long way to go, but all parts are now even more aligned and the future is looking bright!&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s next
&lt;/h3&gt;

&lt;p&gt;In the newly formed designer+developer guild, the work of defining requirements and improvements on the design system continues. From the design side we’re also looking to constantly improve on the documentation and the presentation of our system. This is something we imagine we’ll keep on doing continuously and iteratively for as long as it’s needed, if not forever. After all, “design is never done” and a design system can and should be a living thing constantly evolving along with the products and the brand it serves, and in extension even the promise of the brand and it’s products. In our case, that’s to aid in &lt;strong&gt;accelerating the energy transition towards 100% renewable energy&lt;/strong&gt;. More on how we exactly do that, and how we always aim to design for impact, in the next blog post. Thanks for reading and stay tuned!&lt;/p&gt;

&lt;p&gt;Petter Andersson, Product Designer at Vandebron&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If the type of work mentioned in this blog post sounds interesting to you, &lt;a href="https://werkenbij.vandebron.nl/l/en/"&gt;take a look at our job openings here&lt;/a&gt;.&lt;/em&gt; &lt;/p&gt;

</description>
    </item>
    <item>
      <title>"Fueling" the Energy Transition With Spark - Part 1</title>
      <dc:creator>Rosario</dc:creator>
      <pubDate>Fri, 05 May 2023 09:05:18 +0000</pubDate>
      <link>https://dev.to/vandebron/fueling-the-energy-transition-with-spark-part-1-45h6</link>
      <guid>https://dev.to/vandebron/fueling-the-energy-transition-with-spark-part-1-45h6</guid>
      <description>&lt;p&gt;Here at Vandebron, we have several projects which need to compute large amounts of data. To achieve acceptable results, we had to choose a computing tool that should have helped us to build such algorithms.&lt;/p&gt;

&lt;p&gt;As you may have read in other articles our main backend language is Scala so the natural choice to build distributed parallel algorithms was indeed Spark.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Spark
&lt;/h2&gt;

&lt;p&gt;We will briefly introduce Spark in the next few lines and then we will dive deep into some of its key concepts.&lt;/p&gt;

&lt;p&gt;Spark is an ETL distributed tool. ETL are three phases that describe a general procedure for moving data from a source to a destination.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F32h5krxph24zm2quik6q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F32h5krxph24zm2quik6q.png" alt="ETL Process" width="700" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Extract&lt;/em&gt;&lt;/strong&gt; is the act of retrieving data from a data source which could be a database or a file system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Transform&lt;/em&gt;&lt;/strong&gt; is the core part of an algorithm. As you may know, functional programming is all about transformation. Whenever you write a block of code in Scala you go from an initial data structure to a resulting data structure, the same goes with Spark but the data structures you use are specific Spark structures we will describe later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Load&lt;/em&gt;&lt;/strong&gt; is the final part. Here you need to save (load) the resulting data structure from the transformation phase to a data source. This can either be the same as the extract phase or a different one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Distributed&lt;/em&gt;&lt;/strong&gt;: Spark is meant to be run in a cluster of nodes. Each node runs its own JVM and every Spark data structure can/should be distributed among all the nodes of the cluster (using serialization) to parallelize the computation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Spark data structure: RDD, DataFrame, and Dataset
&lt;/h3&gt;

&lt;p&gt;The core of Spark is its &lt;em&gt;distributed resilient dataset (RDD)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2r7490vvzwzrqna1gtg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2r7490vvzwzrqna1gtg.png" alt="Spark API history" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;&lt;em&gt;RDD&lt;/em&gt;&lt;/strong&gt; is a collection of elements partitioned across the nodes of the cluster that can be operated on in parallel. &lt;em&gt;Extracting&lt;/em&gt; data from a source creates an RDD. Operating on the RDD allows us to &lt;em&gt;transform&lt;/em&gt; the data. Writing the RDD &lt;em&gt;loads&lt;/em&gt; the data into the end target like a database for example). They are made to be distributed over the cluster to parallelize the computation.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;&lt;em&gt;DataFrame&lt;/em&gt;&lt;/strong&gt; is an abstraction on top of an RDD. It is the first attempt of Spark (2013) to organize the data inside and RDD with an SQL-like structure. With dataframe, you can actually make a transformation in an SQL fashion. Every element in a dataframe is a Row and you can actually transform a dataframe to another by adding or removing columns.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;&lt;em&gt;DataSet&lt;/em&gt;&lt;/strong&gt; finally is a further abstraction on top of a dataframe to organize data in an OO fashion (2015). Every element in a dataset is a case class and you can operate transformation in a scala fashion from a case class to another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spark in action
&lt;/h2&gt;

&lt;p&gt;Let’s see now some code samples from our codebase to illustrate in more detail each of the ETL phases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract
&lt;/h3&gt;

&lt;p&gt;The extraction phase is the first step in which you gather the data from a datasource.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;allConnections&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;sparkSession&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;read&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;jdbc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;selectedConnections&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;allConnections&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;select&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;ColumnNames&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;head&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ColumnNames&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;tail&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="k"&gt;_&lt;/span&gt;&lt;span class="kt"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;p4Connections&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;selectedConnections&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;allConnections&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HasP4Day activated"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;equalTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;allConnections&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HasP4INT activated"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;equalTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Connection&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="nv"&gt;p4Connections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;show&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most people the extraction phase is just the first line (the invocation to the read method), they are not wrong because extracting means reading data from a datasource (in this case an SQL server database). I decided to include in this phase also some filtering and projection operations because I think these are not really part of the algorithm, this is still the preparation phase before you actually process the data. We can ultimately say that &lt;em&gt;preparing the data&lt;/em&gt; is something in between extraction and transformation therefore it is up to you to decide which phase it belongs to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transform
&lt;/h3&gt;

&lt;p&gt;Transformation phase is the core of the algorithm. Here you actually process your data to reach your final result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="nv"&gt;usageDF&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;groupBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'ConnectionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;window&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'ReadingDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1 day"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;agg&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'Consumption&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Consumption"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'OffPeak_consumption&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OffPeak_consumption"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'Peak_consumption&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Peak_consumption"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'Production&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Production"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'OffPeak_production&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OffPeak_production"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'Peak_production&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Peak_production"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'ReadingDate&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReadingDate"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'marketsegment&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"marketsegment"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;collect_set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'Source&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sources"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;collect_set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'Tag&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tags"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'Last_modified&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Last_modified"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;withColumn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"Tag"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;array_contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'Tags&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nc"&gt;Interpolated&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
&lt;span class="nf"&gt;lit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Tag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Interpolated&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="py"&gt;otherwise&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;lit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nc"&gt;Measured&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;withColumn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Source"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'Sources&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nf"&gt;lit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Multiple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="py"&gt;otherwise&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mkString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'Sources&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;orderBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;'ConnectionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;'ReadingDate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;drop&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"window"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"sources"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"tags"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this specific example, we are processing connection usage data by aggregating it daily. In the &lt;code&gt;usageDF&lt;/code&gt; we have 15 minutes interval usage data, now we want to show to the user the same data but with a different aggregation interval (1 day). So we group the whole data by connection id and window the reading date by 1 day (A window function calculates a return value for every input row of a table based on a group of rows &lt;a href="https://databricks.com/blog/2015/07/15/introducing-window-functions-in-spark-sql.html"&gt;Introducing Window Functions in Spark SQL - The Databricks Blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once the data is grouped we can aggregate it, using the &lt;code&gt;agg&lt;/code&gt; method which allows us to call the aggregation functions over the dataframe (for example: &lt;code&gt;sum&lt;/code&gt;, &lt;code&gt;first&lt;/code&gt;,&lt;code&gt;max&lt;/code&gt; or &lt;code&gt;collect_set&lt;/code&gt;). Successively we transform the dataframe to suit our visualization needs, the methods used are self-explanatory and the documentation is very clear. &lt;a href="https://spark.apache.org/docs/latest/sql-getting-started.html"&gt;Getting Started - Spark 3.0.1 Documentation&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Load
&lt;/h3&gt;

&lt;p&gt;The final phase is the one which &lt;code&gt;save&lt;/code&gt;, &lt;code&gt;put&lt;/code&gt;, &lt;code&gt;show&lt;/code&gt; the transformed data into the target data source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="nv"&gt;dataFrame&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;select&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;head&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;tail&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="k"&gt;_&lt;/span&gt;&lt;span class="kt"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;write&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;cassandraFormat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keySpace&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saveMode&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;save&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this specific case, we will save our dataframe into a Cassandra database. In Spark, methods used to achieve the load phase are called &lt;em&gt;actions&lt;/em&gt;. It is very important to distinguish Spark actions from the rest because actions are the only ones that trigger Spark to actually perform the whole transformation chain you have defined previously.&lt;/p&gt;

&lt;p&gt;If our transformation phase, as we described above, wasn’t followed by an action (for example &lt;code&gt;save&lt;/code&gt;) nothing would have happened, the software would have simply terminated without doing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  One concept to rule them all
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;rdd1&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;sc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;parallelize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;rdd2&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;sc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;parallelize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;rdd2Count&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;rdd1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;rdd2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;count&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="c1"&gt;//This will NEVER work!!!!&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;One does not simply use RDD inside another RDD&lt;/em&gt;. (Same goes for Dataframes or Datasets).&lt;/p&gt;

&lt;p&gt;This is a very simple concept that leads very often to lots of questions because many people just want to use Spark as a normal scala library. But this is not possible due to the inner distributed nature of Spark and its data structures. We have said that an RDD is a resilient distributed dataset, let’s focus on the word &lt;em&gt;distributed&lt;/em&gt;, it means that the data inside it is spread across the nodes of the cluster. Every node has its own JVM and it is called &lt;em&gt;Executor&lt;/em&gt;, except for the master node where your program starts which is called &lt;em&gt;Driver&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="/images/spark-cluster-overview.png" class="article-body-image-wrapper"&gt;&lt;img src="/images/spark-cluster-overview.png" alt="Spark cluster overview" title="Spark cluster overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your code starts from the Driver and a copy is distributed to all executors, this also means that each executor needs to have the same working environment of the Driver, for Scala it is not a problem since it just needs a JVM to run. (but we will see that if you use &lt;em&gt;pySpark&lt;/em&gt; you need to take extra care when you distribute your application.) Every Spark data structure you have defined in your code will also be distributed across the executors and every time you perform a transformation it will be performed to each chunk of data in each executor.&lt;/p&gt;

&lt;p&gt;Now let’s go back to our example, a &lt;code&gt;map&lt;/code&gt; is a transformation on &lt;code&gt;rdd1&lt;/code&gt; this means that block inside will be executed at the executor level, if we need &lt;code&gt;rdd2&lt;/code&gt; to perform this block Spark should somehow serialize the whole &lt;code&gt;rdd2&lt;/code&gt; and send it to each executor. You can understand now that &lt;em&gt;it is really not possible to serialize the whole RDD since it is by its nature already a distributed data structure&lt;/em&gt;. So what can you do to actually perform such computation we showed in the example? The solution is “simple”: &lt;em&gt;prepare your data in such a way that it will be contained in one single RDD&lt;/em&gt;. To do so you can take advantage of all the transformation functions Spark has to offer such &lt;code&gt;map&lt;/code&gt; &lt;code&gt;join&lt;/code&gt; &lt;code&gt;union&lt;/code&gt; &lt;code&gt;reduce&lt;/code&gt; etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next step…
&lt;/h2&gt;

&lt;p&gt;We have explained all the main concepts of Spark and we have shown some real snippets of our codebase. In the next article, I would like to show you a real-life problem we have solved in our company using &lt;a href="https://spark.apache.org/docs/latest/api/python/index.html"&gt;&lt;em&gt;pySpark&lt;/em&gt;&lt;/a&gt;. I will show you how to customize Spark infrastructure to correctly parallelize the ETL algorithm you have built.&lt;br&gt;
Footer&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Why and How of Dagster User Code Deployment Automation</title>
      <dc:creator>Pieter Custers</dc:creator>
      <pubDate>Mon, 01 May 2023 14:53:58 +0000</pubDate>
      <link>https://dev.to/vandebron/the-why-and-how-of-dagster-user-code-deployment-automation-48a2</link>
      <guid>https://dev.to/vandebron/the-why-and-how-of-dagster-user-code-deployment-automation-48a2</guid>
      <description>&lt;p&gt;This post originally appeared on &lt;a href="https://www.vandebron.tech/blog/cicd-dagster-user-code"&gt;vandebron.tech&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;If you want to deploy new Dagster user code respositories, you need to modify and redeploy the whole Dagster system (while they are &lt;a href="https://docs.dagster.io/deployment/guides/kubernetes/customizing-your-deployment#separately-deploying-dagster-infrastructure-and-user-code"&gt;presented as separate&lt;/a&gt; in the docs). This is undesirable for many reasons, most notably because it slows down a migration or the regular development process. This post presents a way to avoid this and build a fully automated CI/CD-pipeline for (new) user code.&lt;/p&gt;

&lt;p&gt;This article assumes that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you (plan to) host Dagster on Kubernetes and manage its deployment with Helm and Ansible;&lt;/li&gt;
&lt;li&gt;you want to automate the deployment of new Dagster user code repositories with a CI/CD pipeline automation tool of choice;&lt;/li&gt;
&lt;li&gt;and you want to be able to (re)deploy the whole Dagster system and user code from scratch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Dagster?
&lt;/h3&gt;

&lt;p&gt;In short Dagster is a tool to build and orchestrate complex data applications in Python. For us, in the end, Dagster improved the development cycle for things like simple cron jobs as well as for complex ML pipelines. Testing the flows locally was never so easy, for instance. And with features like &lt;a href="https://docs.dagster.io/concepts/assets/asset-materializations"&gt;asset materialization&lt;/a&gt; and &lt;a href="https://docs.dagster.io/concepts/partitions-schedules-sensors/sensors"&gt;sensors&lt;/a&gt;, we can trigger downstream jobs based on the change of an external state that an upstream job caused, without these jobs having to know of each other's existence.&lt;/p&gt;

&lt;p&gt;However, deployment of new &lt;a href="https://docs.dagster.io/concepts/repositories-workspaces/repositories"&gt;user code respositories&lt;/a&gt; caused us some CI/CD related headaches...&lt;/p&gt;

&lt;h3&gt;
  
  
  System and user code are separated
&lt;/h3&gt;

&lt;p&gt;Dagster separates the system deployment - the Dagit (UI) web server and the daemons that coordinate the runs - from the user code deployment - the actual data pipeline. In other words: the user code servers run in complete isolation from the system and each other. &lt;/p&gt;

&lt;p&gt;This is a great feature of which the advantages are obvious: user code repositories have their own Python environment, teams can manage these separately, and if a user code server breaks down the system is not impacted. In fact, it even doesn't require a restart when user code is updated!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EjFd8AQI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/br4s1rax07vkikbcx1yg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EjFd8AQI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/br4s1rax07vkikbcx1yg.png" alt="Schematic of the Dagster architecture. The user code repositories (green) are separate from the rest of the system (yellow and blue). The right side — irrelevant for now — shows the job runs." width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Helm terms: there are 2 charts, namely the &lt;em&gt;system&lt;/em&gt;: &lt;code&gt;dagster/dagster&lt;/code&gt; (&lt;a href="https://github.com/dagster-io/dagster/blob/master/helm/dagster/values.yaml"&gt;values.yaml&lt;/a&gt;), and the &lt;em&gt;user code&lt;/em&gt;: &lt;code&gt;dagster/dagster-user-deployments&lt;/code&gt; (&lt;a href="https://github.com/dagster-io/dagster/blob/master/helm/dagster/charts/dagster-user-deployments/values.yaml"&gt;values.yaml&lt;/a&gt;). Note that you have to set &lt;code&gt;dagster-user-deployments.enabled: true&lt;/code&gt; in the &lt;code&gt;dagster/dagster&lt;/code&gt; values-yaml to enable this.&lt;/p&gt;

&lt;h4&gt;
  
  
  Or are they?
&lt;/h4&gt;

&lt;p&gt;That having said, you might find it peculiar that in the values-yaml of the system deployment, &lt;em&gt;you need to specify the user code servers&lt;/em&gt;. That looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;workspace:
    enabled: true
    servers:
      - host: "k8s-example-user-code-1"
        port: 3030
        name: "user-code-example"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This means system and user deployments are not actually completely separated!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This implies that, if you want to add a &lt;em&gt;new&lt;/em&gt; user code repository, not only do you need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;add the repo to the user code's &lt;code&gt;values.yaml&lt;/code&gt; (via a PR in the Git repo of your company's platform team, probably);&lt;/li&gt;
&lt;li&gt;do a helm-upgrade of the corresponding &lt;code&gt;dagster/dagster-user-deployments&lt;/code&gt; chart;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;but because of the not-so-separation, you still need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;add the user code server to the system's &lt;code&gt;values.yaml&lt;/code&gt; (via that same PR);&lt;/li&gt;
&lt;li&gt;and do a helm-upgrade of the corresponding &lt;code&gt;dagster/dagster&lt;/code&gt; chart.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Formally this is the process to go through. If you are fine with this, stop reading here. It's the cleanest solution anyway. But it is quite cumbersome, so...&lt;/p&gt;

&lt;p&gt;If you are in a situation in which new repositories can get added multiple times a day - for instance because you are in the middle of a migration to Dagster, or you want a staging environment for every single PR - then read on.&lt;/p&gt;

&lt;h4&gt;
  
  
  Give me more details
&lt;/h4&gt;

&lt;p&gt;How it works is that &lt;a href="https://docs.dagster.io/deployment/guides/kubernetes/deploying-with-helm#user-code-deployment"&gt;for every new repo Dagster spins up a (gRPC) server to host the user code&lt;/a&gt;. The separation is clear here. But the Dagster &lt;em&gt;system&lt;/em&gt; also needs to know about these user code servers, and it does so through a workspace-yaml file. If you run Dagit locally it relies on a &lt;code&gt;workspace.yaml&lt;/code&gt; file; on Kubernetes it relies on a &lt;a href="https://kubernetes.io/docs/concepts/configuration/configmap/"&gt;ConfigMap&lt;/a&gt; - a Kubernetes object used to store non-confidential data in key-value pairs, e.g. the content of a file - which they named &lt;code&gt;dagster-workspace-yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This workspace-yaml is the connection between the system and the user code. The fact that the charts are designed as such that this workspace-yaml is created and modified through the system deployment rather than the user code deployment is the reason we need to redeploy the system. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But what if we could modify this workspace-yaml file ourselves? Can we make the system redeployment obsolete? Short answer: we can.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Our solution
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: what we present here is a workaround that we'll keep in place until the moment Dagster releases a version in which the Dagster user code deployment is &lt;strong&gt;actually completely separated&lt;/strong&gt; from the system deployment. And it works like a charm.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remember: the desired situation is that we do not have to edit the values-yaml files (through a PR) and redeploy all of Dagster for every new repo.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First of all, we added an extra ConfigMap in Kubernetes that contains the &lt;code&gt;values.yaml&lt;/code&gt; for the &lt;code&gt;dagster/dagster-user-deployments&lt;/code&gt; chart. We named it &lt;code&gt;dagster-user-deployments-values-yaml&lt;/code&gt;. The fact that this is a ConfigMap is crucial to prevent conflicts (see next section).&lt;/p&gt;

&lt;p&gt;With the extra ConfigMap in place, these are the steps when a repo gets added:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the new repo to the &lt;code&gt;dagster-user-deployments-values-yaml&lt;/code&gt; Configmap.&lt;/li&gt;
&lt;li&gt;Helm-upgrade the &lt;code&gt;dagster/dagster-user-deployments&lt;/code&gt; chart with the content of that ConfigMap.&lt;/li&gt;
&lt;li&gt;Add the server to the &lt;code&gt;dagster-workspace-yaml&lt;/code&gt; ConfigMap.&lt;/li&gt;
&lt;li&gt;Do a rolling restart of the &lt;code&gt;dagster-dagit&lt;/code&gt; and &lt;code&gt;dagster-daemon&lt;/code&gt; deployment to pull the latest workspace to these services.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Refresh the workspace in the UI and there it is, your new repo!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The steps above are completely automatable through your favorite CI/CD pipeline automation tool.&lt;/li&gt;
&lt;li&gt;There is no interaction with a (platform team) Git repo.&lt;/li&gt;
&lt;li&gt;The process, unfortunately, still requires a restart of the system in order to pull the latest workspace-yaml to the system services. The daemon terminates, then restarts, and it might cause a short interruption. Note that this is unavoidable if you add a new repo, no matter how you add it. This could be avoided if a reload of the ConfigMap would be triggered upon a change, &lt;a href="https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically"&gt;which is possible&lt;/a&gt; but not enabled.&lt;/li&gt;
&lt;li&gt;If you want to make changes to an existing repo (not code changes but server setting changes), you only have to do the first step (and &lt;em&gt;modify&lt;/em&gt; instead of &lt;em&gt;add&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  How to prevent conflicts
&lt;/h4&gt;

&lt;p&gt;With many of your team members adding new Dagster repositories through an automated CI/CD pipeline, you might face the situation that 2 people are adding a new repo at around the same time. &lt;/p&gt;

&lt;p&gt;When this happens, the &lt;code&gt;dagster-user-deployments-values-yaml&lt;/code&gt; ConfigMap cannot be uploaded in the first step because Kubernetes demands that you provide the &lt;em&gt;last-applied-configuration&lt;/em&gt; when doing an update. If it doesn't match, the upload fails. &lt;/p&gt;

&lt;p&gt;This is perfect as we do not want to overwrite the changes of the conflicting flow. You can optionally build in a retry-mechanism that starts over with pulling the ConfigMap again.&lt;/p&gt;

&lt;h4&gt;
  
  
  How to deploy from scratch
&lt;/h4&gt;

&lt;p&gt;The above does not yet cover how we are able to deploy the Dagster system &lt;em&gt;and user code&lt;/em&gt; completely from scratch. Why do we want this? Well, for instance when somebody accidently deletes the &lt;code&gt;dagster&lt;/code&gt; namespace for instance. Or hell breaks loose in any other physical or non-physical form. Or when we simply want to bump the Dagster version, actually.&lt;/p&gt;

&lt;p&gt;The key to this is that we version both the &lt;code&gt;dagster-user-deployments-values-yaml&lt;/code&gt; and &lt;code&gt;dagster-workspace-yaml&lt;/code&gt; as a final step to the flow described above (we do it on S3, in a versioned bucket). Whenever we redeploy Dagster (with Ansible) we pull the latest versions and use them to compile both the values-yaml files from it. &lt;/p&gt;

&lt;h4&gt;
  
  
  How to clean up old repositories
&lt;/h4&gt;

&lt;p&gt;The above described automation &lt;em&gt;adds&lt;/em&gt; new repos but doesn't take care of old obsolete repos. The steps for removing a repo are the same for adding one. The exact implementation depends on your situation. You might want to automatically remove PR staging environments after closing a PR, for instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Dagster is an incredibly powerful tool that enabled us to build complex data pipelines with ease. This posts explains how we &lt;strong&gt;streamlined the CI/CD pipeline for user code respositories&lt;/strong&gt;, which enabled us to migrate to Dagster very quickly and saves us lots of time on a daily basis.&lt;/p&gt;

</description>
      <category>dagster</category>
      <category>cicd</category>
    </item>
    <item>
      <title>How to Spin Up A Kubernetes Cluster On Your Macbook</title>
      <dc:creator>Vandebron Technology</dc:creator>
      <pubDate>Tue, 25 Apr 2023 19:12:35 +0000</pubDate>
      <link>https://dev.to/vandebron/how-to-spin-up-a-kubernetes-cluster-on-your-macbook-kbp</link>
      <guid>https://dev.to/vandebron/how-to-spin-up-a-kubernetes-cluster-on-your-macbook-kbp</guid>
      <description>&lt;p&gt;In Vandebron we have been using container clusters to host our services since the foundation of our Big Data team. Recently our cluster of choice has declared End-Of-Life development stage, so we decided to take a step forward and get a ticket for the Kubernetes boat.&lt;/p&gt;

&lt;p&gt;A change in the OS that is used to run your services and applications can look quite challenging and not everyone is on the same experience level. To make everyone comfortable it is a good choice to give everyone the possibility to play with the new tools and learn what can be done and how: &lt;strong&gt;you need a sandbox.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our developers are provided with a Macbook and at the moment of writing there some options you can go for when deciding how to set up your playground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Docker CE Kubernetes&lt;/strong&gt;: This is the easiest solution since there is a handy button to run your containers into a Kubernetes environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vagrant and Virtualbox&lt;/strong&gt;: This solution is the one that can give you more control and you can easily create a cluster the size you want, but you need to be handy with VMs, the hypervisor of choice, and Vagrant. It's the old school way to do it but, while it's a chunk your platform engineers can bite, it can be a steep and frustrating process for people that are not used to handle VMs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multipass + some bash magic glue&lt;/strong&gt;: Since Canonical created this tool for macOS, creating an Ubuntu VM became a breeze and you can have a single, easily manageable VM with its networking up and running in less than a minute, without having to handle disks, distros, and stuff. On top of it, the command line interface is straight forward and it has just the basic commands we will need, so wrapping the entire process into a bash script is a piece of cake.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have found this super cool in-depth &lt;a href="https://jyeee.medium.com/kubernetes-on-your-macos-laptop-with-multipass-k3s-and-rancher-2-4-6e9cbf013f58"&gt;article&lt;/a&gt; from Jason Yee (kudos to you bruh) that guided me through the installation of my first single node Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;The process is not that long but it involves a lot of copy/pasting and, once learned the basics, I didn't want to go under the same process more times, plus it could be interesting for me as a Platform Engineer, but it may be boring and pointless for developers who just want to have a sandbox replica of what they are working on in the remote environment. My automator (aka do-it-once-never-do-it-again) spirit kicked in and I decided to wrap every step in a small command-line tool with only 3 options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;install&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;cleanup&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;help&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What is happening under the hood
&lt;/h3&gt;

&lt;p&gt;What the script does is substantially automating all the steps needed to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Create a new VM using Multipass (tool released by Canonical)&lt;/li&gt;
&lt;li&gt; Fetch the VM IP address and adding it to your local &lt;code&gt;/etc/hosts&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt; Install k3s (a lightweight distribution of Kubernetes) on top of the VM&lt;/li&gt;
&lt;li&gt; Install the Kubernetes command-line tools on your laptop&lt;/li&gt;
&lt;li&gt; Install Helm (the Kubernetes package manager) on your laptop&lt;/li&gt;
&lt;li&gt; Install cert-manager (certificate manager) package on top of your k3s cluster&lt;/li&gt;
&lt;li&gt; Install Rancher (a Kubernetes control plane) package on top of your k3s cluster&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are looking for a more in-depth breakdown of the single steps you can download and inspect &lt;a href="https://gist.githubusercontent.com/nikotrone/50b1a5f8d137411879eb2467e689bfbe/raw/090b4b4323d96ac28d96bbb346e2e657073722e6/bronernetes"&gt;the script&lt;/a&gt; (one of the many advantages of &lt;a href="https://en.wikipedia.org/wiki/Open_source"&gt;OpenSource&lt;/a&gt; projects) or checkout and read the original &lt;a href="https://jyeee.medium.com/kubernetes-on-your-macos-laptop-with-multipass-k3s-and-rancher-2-4-6e9cbf013f58"&gt;article&lt;/a&gt;: it explains line by line what the specific commands are doing.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Multipass VM
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://multipass.run/"&gt;Multipass&lt;/a&gt; is a tool from Canonical (the company developing and maintaining the Ubuntu Linux distribution) that leverages Hyperkit (macOS feature to handle virtualization) to create and handle a Virtual Machine directly on your Mac.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Edit /etc/hosts
&lt;/h4&gt;

&lt;p&gt;Once we have our VM up and running we need to make it available with an easy url that is also gonna be used to generate the SSL certificate, in our case we picked up &lt;code&gt;rancher.localdev&lt;/code&gt;. It is important to have a name setup in the beginning since this one will need to match with the certificate so we can use it programmatically.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Install K3S
&lt;/h4&gt;

&lt;p&gt;This step is pretty straightforward: just fetch a script that is publicly available on the &lt;a href="https://get.k3s.io"&gt;k3s official website&lt;/a&gt; and feed it to your bash. K3s is a lightweight version of Kubernetes with all the needed dependencies and executable packaged in a convenient installation script. Because of its light nature, it is often used in embedded devices that have a limited amount of resources to offer.&lt;/p&gt;

&lt;h4&gt;
  
  
  4 &amp;amp; 5. Kubernetes and Helm cli
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes cli&lt;/strong&gt; (&lt;code&gt;kubectl&lt;/code&gt;) is used to talk and interact with your Kubernetes cluster. It can be used to manage multiple clusters according to the content of your KUBECONFIG environment variable. The variable itself contains just a path to where your cluster configuration is stored, so you can switch from a cluster to another by simply pointing to another file that contains the configuration of another cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Helm&lt;/strong&gt; instead is the "package manager" of Kubernetes: you can use it to add repositories to specific &lt;code&gt;charts&lt;/code&gt; which are the blueprint that contains a way to install a specific tool on your cluster. Both of these tools have to be installed and run from your local laptop, either in the case you are managing a local VM or in the case you are interacting with a remote cluster.&lt;/p&gt;

&lt;h4&gt;
  
  
  6 &amp;amp; 7. cert-manager and Rancher
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Rancher&lt;/strong&gt; is the control plane for our cluster: it provides a GUI and an overview of our single node cluster. It offers other goodies like management of multiple clusters, deployed on different locations like AWS Azure and GCP or even on your own hardware, plus certificate deployment and some other handy functionalities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cert-manager&lt;/strong&gt; is installed via Helm chart and it is the tool used by Rancher to generate and deploy a certificate across the entire cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to use it
&lt;/h3&gt;

&lt;p&gt;All the steps will involve the use of a Terminal window&lt;/p&gt;

&lt;h4&gt;
  
  
  Installation
&lt;/h4&gt;

&lt;p&gt;The first thing you need to do is download &lt;a href="https://gist.github.com/nikotrone/50b1a5f8d137411879eb2467e689bfbe"&gt;this script&lt;/a&gt; and save it in a folder on your Mac (let's assume &lt;code&gt;~/bronernetes&lt;/code&gt;) by executing&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    &lt;span class="sb"&gt;```&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;% endraw %&lt;span class="o"&gt;}&lt;/span&gt;
bash
&lt;span class="nb"&gt;mkdir&lt;/span&gt; ~/bronernetes
    &lt;span class="nb"&gt;cd&lt;/span&gt; ~/bronernetes
    curl https://gist.githubusercontent.com/nikotrone/50b1a5f8d137411879eb2467e689bfbe/raw/090b4b4323d96ac28d96bbb346e2e657073722e6/bronernetes &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; bronernetes
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;:&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;% raw %&lt;span class="o"&gt;}&lt;/span&gt;

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

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Now we have the toolset and you can confirm it works by simply running `bronernetes help`.

#### Spin up Kubernetes

The next step is to run the installation process with the command `bronernetes install`

#### Clean up

When you are done or you just want to hard reset your environment you can just type `bronernetes cleanup` and it will take care of cleaning up the VM you just used, leaving you with a pristine machine, as nothing ever happened :)

### Conclusion

Having a sandbox is very useful to play around with the concepts of a new setup or service and it packs up a huge amount of positive sides. No matter what is the language or the nature of the system you are trying to replicate, it can be challenging and involve a long list of instructions or manual operations and, sometimes, even dedicated hardware. Although with some bash glue, it is possible to automate most of those processes and the investment cost can be enormously beneficial for yourself (less work the next time you do it) and for the other people working with you (they can use the tool, comment and suggest improvements). Most of all, in the case of infrastructure, it helps raise the knowledge of "what's going on here" and documents for the ones interested in taking a trip down the rabbit hole.

***

This post was originally published on [vandebron.tech](https://vandebron.tech/blog/spin-up-kubernetes-on-macbook). Reposted automatically with [Reposted.io](https://reposted.io?utm_source=postFooter). 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
    </item>
    <item>
      <title>Cypress.io Component Design Technique for React Applications</title>
      <dc:creator>Vandebron Technology</dc:creator>
      <pubDate>Tue, 25 Apr 2023 19:11:32 +0000</pubDate>
      <link>https://dev.to/vandebron/cypressio-component-design-technique-for-react-applications-569k</link>
      <guid>https://dev.to/vandebron/cypressio-component-design-technique-for-react-applications-569k</guid>
      <description>&lt;p&gt;Cypress is a game-changer in the automation testing world, the way that Cypress was built and its architecture allows us as testers to cover more scenarios.&lt;/p&gt;

&lt;p&gt;Cypress is not Selenium; in fact, it is different. And the way to build and design a framework should be different as well.&lt;/p&gt;

&lt;p&gt;The most famous design technique in Selenium is the Page Object Model, and many testers use the same design technique with Cypress. Even that Cypress on their official website &lt;a href="https://www.cypress.io/blog/2019/01/03/stop-using-page-objects-and-start-using-app-actions/"&gt;recommended&lt;/a&gt; us not to go with that approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Page Object Model
&lt;/h2&gt;

&lt;p&gt;The main benefit of using the page object model Is to make the automation framework maintenance-friendly. We can define a specific page's selectors in a separate file and then use these selectors in our test cases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;SignInPage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/signin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;getEmailError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-testid=SignInEmailError]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;getPasswordError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-testid=SignInPasswordError]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;fillEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-testid=SignInEmailField]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;fillPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-testid=SignInPasswordField]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-testid=SignInSubmitButton]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;SignInPage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main two downsides using the typical page object model with cypress are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Page objects introduce an additional state into the tests, separate from the application’s internal state. This makes understanding the tests and failures harder.&lt;/li&gt;
&lt;li&gt;  Page objects make tests slow because they force the tests to always go through the application user interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Component-Based Architecture
&lt;/h2&gt;

&lt;p&gt;On the other hand, a React application is component-based, where a specific page will be built from a collection of components. And components in React can be used on different pages too. So if we want to use the Page Object Model, we may define the same locator twice on different pages.&lt;/p&gt;

&lt;p&gt;So having these two facts, At Vandebron, we came up with a new way to design our Cypress Automation framework by creating a separate JavaScript file for every component in our application, inside a folder called &lt;code&gt;components&lt;/code&gt; within our Cypress project as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Locators&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getEmailError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-testid=SignInEmailError]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getPasswordError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-testid=SignInPasswordError]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-testid=SignInEmailField]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;passwordField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-testid=SignInPasswordField]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-testid=SignInSubmitButton]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Actions&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/signin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;performLogin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;emailField&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;passwordField&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;submitButton&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having it built this way, we eliminated all the previous problems mentioned earlier; we are not adding any classes, and we are defining objects within our test cases. And the most important part is that we are following the way that Cypress recommends it.&lt;/p&gt;

&lt;p&gt;And after defining the component locators and actions, we can import them inside our test case and use them as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;LoginComponent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../components/loginComponent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Menu&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../components/Menu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Test Login Page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should show an error message if the password in wrong&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;LoginComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;LoginComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;performLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email@gmail.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wrongPassword&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;LoginComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getPasswordError&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should show the logout button if the user logged in succesfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;LoginComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;LoginComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;performLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email@gmail.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;correctPassword&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;Menu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LogoutButton&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And as you can see, our test cases are readable for anyone! And if any locator changes in any of the components, we can easily fix it in one location and from the same file. And lastly, if a component will be used in different places, we can use the same code.&lt;/p&gt;

&lt;p&gt;In the next article, I will talk about how we use Cypress in our manual testing during the sprint and how it saves us tons of time and effort.&lt;/p&gt;




&lt;p&gt;This post was originally published on &lt;a href="https://vandebron.tech/blog/cypress-component-design-technique-for-react-applications"&gt;vandebron.tech&lt;/a&gt;. Reposted automatically with &lt;a href="https://reposted.io?utm_source=postFooter"&gt;Reposted.io&lt;/a&gt;. &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Migrating from DCOS to Kubernetes, dealing with the l4lb loadbalancer</title>
      <dc:creator>Vandebron Technology</dc:creator>
      <pubDate>Tue, 25 Apr 2023 18:55:39 +0000</pubDate>
      <link>https://dev.to/vandebron/migrating-from-dcos-to-kubernetes-dealing-with-the-l4lb-loadbalancer-2ok4</link>
      <guid>https://dev.to/vandebron/migrating-from-dcos-to-kubernetes-dealing-with-the-l4lb-loadbalancer-2ok4</guid>
      <description>&lt;p&gt;In October 2020 D2IQ &lt;a href="https://d2iq.com/blog/d2iq-takes-the-next-step-forward"&gt;announced&lt;/a&gt; that they are moving onwards with their Kubernetes offering. Vandebron has been a D2IQ customer for their DCOS offering, we were just in the middle of a migration of our first workloads to DCOS Enterprise. We have evaluated the D2IQ K8s offering and decided to go for another Kubernetes product. We had a few migrations over the years, we migrated from Azure to AWS, we migrated workloads from normal instances to spot instances and all these migrations were done with nearly any downtime. We plan to reduce the downtime to a couple of minutes this migration and this is a real challenge. The first challenge that we will discuss today: We want to pair our Kubernetes clusters to the DCOS/Mesos clusters, while we move a workload it should be able to connect to its dependencies in the DCOS cluster. We use DCOS for our NoSQL databases like Cassandra, internal data that we want to keep internal. Pairing DCOS and Kubernetes clusters enable us to reduce downtime, enabling us to switch back if we run into issues and move faster because it reduces complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  L4LB
&lt;/h2&gt;

&lt;p&gt;The internal layer 4 load balancer DCOS provides is used in the majority of our workloads. When our data scientists schedule a spark driver, they connect to the spark dispatcher through the Layer 4 load balancer. Most of the DCOS frameworks use this Layer 4 load balancer as an internal service discovery tool, with Vandebron we use this layer 4 load balancer to communicate between services. In a default DCOS set up this load balancer responds on domain names like: &lt;code&gt;spark-dispatcher.marathon.l4lb.thisdcos.directory:7077&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When we ping the spark dispatcher we get the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;PING spark-dispatcher.marathon.l4lb.thisdcos.directory &lt;span class="o"&gt;(&lt;/span&gt;11.155.161.35&lt;span class="o"&gt;)&lt;/span&gt; 56&lt;span class="o"&gt;(&lt;/span&gt;84&lt;span class="o"&gt;)&lt;/span&gt; bytes of data.
64 bytes from 11.155.161.35 &lt;span class="o"&gt;(&lt;/span&gt;11.155.161.35&lt;span class="o"&gt;)&lt;/span&gt;: &lt;span class="nv"&gt;icmp_seq&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;64 &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.024 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After some investigation we found out that this IP range is not actually on a network interface, it is a Linux kernel functionality called &lt;code&gt;IPVS&lt;/code&gt;. With IPVS you can do layer 4 load balancing, you provide the target location and the location you want to respond on.&lt;/p&gt;

&lt;p&gt;When we search for the IP from the spark dispatcher with ipvsadm, we get 3 results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ipvsadm &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; |grep &lt;span class="nt"&gt;--color&lt;/span&gt; &lt;span class="s1"&gt;'11.155.161.35\|$'&lt;/span&gt;
TCP  11.155.161.35:80 wlc
  -&amp;gt; 10.2.7.146:16827             Masq    1      0          0
TCP  11.155.161.35:4040 wlc
  -&amp;gt; 10.2.7.146:16826             Masq    1      0          0
TCP  11.155.161.35:7077 wlc
  -&amp;gt; 10.2.7.146:16825             Masq    1      0          0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see the IP &lt;code&gt;11.155.161.35&lt;/code&gt; points towards &lt;code&gt;10.2.7.146&lt;/code&gt;, even the ports are configured and forwarded. We can add our route with ipvsadm, to understand IPVS a bit better. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ipvsadm &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; 1.2.3.4:80 &lt;span class="nt"&gt;-s&lt;/span&gt; wlc &lt;span class="c"&gt;# we add the target server and assign the scheduler&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ipvsadm &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; 10.2.7.146:16825 &lt;span class="nt"&gt;-t&lt;/span&gt; 1.2.3.4:80 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="c"&gt;# we configure the real server and target server and configure Masquerading&lt;/span&gt;
curl 1.2.3.4:80
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"action"&lt;/span&gt; : &lt;span class="s2"&gt;"ErrorResponse"&lt;/span&gt;,
  &lt;span class="s2"&gt;"message"&lt;/span&gt; : &lt;span class="s2"&gt;"Missing protocol version. Please submit requests through http://[host]:[port]/v1/submissions/..."&lt;/span&gt;,
  &lt;span class="s2"&gt;"serverSparkVersion"&lt;/span&gt; : &lt;span class="s2"&gt;"2.3.4"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This results in that the spark dispatcher now also is available on &lt;code&gt;1.2.3.4:80&lt;/code&gt;. As mentioned before we wanted to connect our DCOS and Kubernetes clusters, getting hundreds of entries from ipvsadm and manually adding them one by one didn’t sound appealing to us. Especially if you consider that sometimes services fail and run on a different port or different host after recovery, maintaining this by hand would be a nightmare. We therefore decided to build a tool to sync IPVS entries from DCOS to Kubernetes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;Within Vandebron we have our tech stack, we strongly believe it is good to eat your own dog food. When possible and when our use cases are similar we use the same tools as our Developers use. The parts of the stack we will be using are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  AWS ELB in front of Traefik 1.7&lt;/li&gt;
&lt;li&gt;  DCOS&lt;/li&gt;
&lt;li&gt;  Kubernetes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Within our platform team, we use Golang as our scripting language. Golang gives us the ability to build binary files with all the required libraries in the binary, we don’t have to install any packages, we do not even need to install Golang on the machine the application will be running on.&lt;/p&gt;

&lt;p&gt;In our DCOS cluster we use Traefik 1.7, this version of Traefik only forwards HTTP requests. We decided to use Traefik to expose a JSON endpoint so we can gather the IPVS information from this location.&lt;/p&gt;

&lt;h2&gt;
  
  
  ipvs-server
&lt;/h2&gt;

&lt;p&gt;Within our DCOS cluster we will expose the IPVS information through a JSON endpoint. We have built a tool for this to expose this information in multiple ways. In the next section, we are going to discuss some of the concepts and choices we made, we won’t deep dive into Go specifics. We have provided the entire code for this project in the examples directory of our GitHub repo: &lt;a href="https://github.com/Vandebron/tech-blog"&gt;https://github.com/Vandebron/tech-blog&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, let’s discuss the library we use: &lt;a href="https://github.com/nanobox-io/golang-lvs"&gt;https://github.com/nanobox-io/golang-lvs&lt;/a&gt;. This library in its essence translates to ipvsadm commands, it helped save us time to implement this ourselves. There are some gotcha’s, such as newlines are not filtered out from the output. We solved this by cleaning up some of the data.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;childChan&lt;/code&gt; function we create a go channel that is responsible for polling &lt;code&gt;ipvsadm&lt;/code&gt; every 10 seconds and stores the result in a couple of variables we use in our HTTP endpoints. IPVS is a Linux kernel functionality and should be highly performant, we do not want to trigger kernel panics when the server gets overloaded with requests. We expect that every 10 seconds gives us accurate enough results, we can always lower this interval to ensure faster results. We also added in this function the string manipulation to ensure all the newlines were gone in the JSON output. The newline gave issues when we tried to add the IPVS scheduler entries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;childChan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Starting time based IPVS Admin poll"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="n"&gt;pollInterval&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
   &lt;span class="n"&gt;timerCh&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pollInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="c"&gt;// Time based loop to generate Global variable&lt;/span&gt;
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;timerCh&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="c"&gt;// when shutdown is received we break&lt;/span&gt;
       &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
           &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Received shutdown, stopping timer"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;break&lt;/span&gt;
       &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
           &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
           &lt;span class="n"&gt;listIpvs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="n"&gt;ipvsString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listIpvs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

           &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;responseObject&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;listIpvs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="p"&gt;}&lt;/span&gt;

           &lt;span class="n"&gt;ipvsJSONbyte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="n"&gt;logToErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR: -- Marshal JSON -- %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;}&lt;/span&gt;

           &lt;span class="n"&gt;ipvsString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ipvsJSONbyte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;ipvsJSON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ipvsString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`\n`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;``&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DEBUG: -- ipvsJSON --"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ipvsJSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next is the index handler, we set our headers correctly and print the result as we would receive through ipvsadm. The index is mainly for our platform engineers to debug and verify the output. Thanks to this overview we found much faster that there was a newline hidden in the scheduler output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="c"&gt;// Generating the Index&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

       &lt;span class="c"&gt;// Only available when debug is on&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DEBUG: -- index --"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ipvsString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;

       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;return&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"text/plain; charset=utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="c"&gt;// Site security testers expect this header to be set&lt;/span&gt;
       &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Content-Type-Options"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ipvsString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON endpoint is what we use in the client communicate with the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;jsonz&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="c"&gt;// Generating the Index&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

       &lt;span class="c"&gt;// Only available when debug is on&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DEBUG: -- jsonz --"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ipvsJSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;

       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"/json"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;return&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json; charset=utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="c"&gt;// Site security testers expect this header to be set&lt;/span&gt;
       &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Content-Type-Options"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ipvsJSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We ask our Developers often to implement a basic health endpoint, in DCOS we use this to see if a service needs to be restarted. In our application we enable set the statusOK in the index or in the JSON endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;healthz&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="c"&gt;// Generating the healthz endpoint&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadInt32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNoContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;return&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusServiceUnavailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our logging and tracing functions we want to register the clients that are connecting, this gives us information where calls are coming from. It helps us debugging if we see weird behaviour.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextRequestID&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="c"&gt;// Tracing the http requests so its easier to check if server is reached&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;requestID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Request-Id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;requestID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="n"&gt;requestID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nextRequestID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="p"&gt;}&lt;/span&gt;
           &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;requestIDKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Request-Id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
       &lt;span class="p"&gt;})&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logToOut&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="c"&gt;// Creating logging entry tracing the http requests&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="n"&gt;requestID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestIDKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                   &lt;span class="n"&gt;requestID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;
               &lt;span class="p"&gt;}&lt;/span&gt;
               &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoteAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserAgent&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
           &lt;span class="p"&gt;}()&lt;/span&gt;
           &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;})&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IPVS needs to be executed with root privileges, to ensure this is correct we get the userid and print it when starting the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// getProcessOwner function is to see who is running the process. It needs to be a sudo / root user&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;getProcessOwner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-o"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Itoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getpid&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;logToErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR: -- getProcessOwner -- %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We added the init function to ensure we print results the moment the server starts up, if we would not do this it would take 10 seconds for the go channel to activate&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="c"&gt;// Placing the Save and val in the init, else we will need to wait for channel to perform its first run&lt;/span&gt;
   &lt;span class="n"&gt;listIpvs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="n"&gt;ipvsString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listIpvs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the main function, we set the configurable flags, such as debugging to show error messages. It proved useful during the creation of this tool to keep track and print output. If we would print the output at every call to our logs, our Elastic cluster would get thousands of logs that add little to no value.&lt;/p&gt;

&lt;p&gt;We configure the listen port in the flags, we can use the portIndex from DCOS to assign a random port on the host to listen on. We also provided to print the version we are running. In our versioning, we use a constant to list the application semver version, we also provide the git-commit hash. When we begin the server we print the version information, the port we listen on and the user running the process. We then start the server process with the go channel, in setting up the go channel we ensure that when the server stops we try to gracefully stop the server within a 30-second timeframe. Since our ipvsadm timer is 10 seconds it should be able to cleanly shutdown within that period.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker build
&lt;/h3&gt;

&lt;p&gt;In the repository, we have included a Dockerfile and a script to build the Dockerfile. In this Dockerfile, we pass the git commit hash to the go install. This way we always get the Git Hash from our GitHub repo and we can use this information in our version output.&lt;/p&gt;

&lt;h3&gt;
  
  
  DCOS service.json
&lt;/h3&gt;

&lt;p&gt;In the repository, we have provided the service.json file, since it is opinionated on using Traefik you might need to change it. But in this service.json you see how we set up Traefik, the health check, and port index. Since the Mesos UCR container has fewer abstractions and has fewer limited capabilities. We can run the IPVS server inside a UCR container and get all the output as if we were running this directly as root on the host machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  ipvs-client
&lt;/h2&gt;

&lt;p&gt;The IPVS client is the component we use in the Kubernetes environment. The client connects to the server and gets the IPVS entries from the IPVS server inside our DCOS cluster. It then adds these IPVS entries to each node in the Kubernetes cluster. You, therefore, need to run each client per Kubernetes node.&lt;/p&gt;

&lt;p&gt;You can find the code from the IPVS client in our repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;httpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseRequestURI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remoteURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;logToErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR: -- new HTTP request -- %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="n"&gt;ipvsClient&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// Timeout after 2 seconds&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"User-Agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"go-ipvs-get &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s"&gt;version: "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s"&gt; Git Commit: "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;gitCommit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ipvsClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;logToErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR: -- ipvsClient -- %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ioutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;readErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;logToErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR: -- body -- %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the httpGet function we can debug the URL and check if it is valid. Again we set the correct headers and retrieve the JSON body.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;lvs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

   &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;responseObject&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;listIpvs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="n"&gt;jsonErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;jsonErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;logToErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR: -- Unmarshal -- %v &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DEBUG: -- res -- %v &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;

   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the unmarshal function we unmarshal the JSON and turn it in a slice of lvs.Service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;addServers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteAddr&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteAddr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;jsonData&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jsonData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DEBUG: -- range jsonDATA --&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ipvsCount=%v, value=%v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;

       &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;lvs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultIpvs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;logToErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR: -- AddService -- %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;

       &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
       &lt;span class="n"&gt;ipvsServerCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the addServers function we add the servers to IPVS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;clientChan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Starting time based IPVS Admin add"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="n"&gt;pollInterval&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
   &lt;span class="n"&gt;timerCh&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pollInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="c"&gt;// Time based loop to generate Global variable&lt;/span&gt;
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;timerCh&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="c"&gt;// when shutdown is received we break&lt;/span&gt;
       &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
           &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Received shutdown, stopping timer"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;break&lt;/span&gt;
       &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;

           &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Clearing &amp;amp; Adding servers..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="c"&gt;// Before we add Servers we need to clear the existing list&lt;/span&gt;
           &lt;span class="n"&gt;lvs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="n"&gt;addServers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteAddr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="n"&gt;logToOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IPVS servers added:&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s"&gt;%v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ipvsServerCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like we did in the IPVS server we create a go channel to poll every 10 seconds the server endpoint. We perform this to get at a set interval the IPVS entries.&lt;/p&gt;

&lt;p&gt;Since we run the IPVS client as a binary directly on the Kubernetes hosts we build the binary with a few parameters we pass to the go build command. The binary we build with this command we host on an internal s3 bucket, we can download this binary with systemd unit files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux
&lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64
&lt;span class="nv"&gt;GIT_COMMIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-list &lt;span class="nt"&gt;-1&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;GOOS
&lt;span class="nb"&gt;export &lt;/span&gt;GOARCH
&lt;span class="nb"&gt;export &lt;/span&gt;GIT_COMMIT

&lt;span class="nb"&gt;env &lt;/span&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; go build &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-ldflags&lt;/span&gt; &lt;span class="s2"&gt;"-X main.gitCommit=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GIT_COMMIT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we run the IPVS client we can verify if the IPVS routes are added by running the &lt;code&gt;ipvsadm -L -n&lt;/code&gt; command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit files
&lt;/h3&gt;

&lt;p&gt;Since IPVS is part of the Linux kernel it is hard to deploy this in a docker container, the capabilities are more restricted in Kubernetes. We decided to deploy the IPVS client on each host machine through a systemd unit file, the main reason was that we ran into restrictions that slowed us down and this is not a permanent solution. By adding the IPVS client on the machines alone does not make it possible for containers to use the IPVS routes. We needed to add NET_ADMIN capabilities to all containers using the l4lb loadbalancer locations and configure &lt;code&gt;hostNetworking: true&lt;/code&gt; in the Kubernetes pods.&lt;/p&gt;

&lt;p&gt;We provided a deployment.yml file that runs a Ubuntu docker container with ipvsadm only installed extra. When the pods are deployed in this deployment you can use kubectl exec to get into the pod and run the &lt;code&gt;ipvsadm -L -n&lt;/code&gt; command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vacancy at Vandebron
&lt;/h2&gt;

&lt;p&gt;We are looking for a platform engineer in Vandebron. As you can understand this is not a typical scenario we daily run across, but it is part of the workloads that we will support when working on our platform. Within Vandebron we try to use the best technology available, when it is not available we build it. Due to this as platform engineers, we have many interesting challenges and offer engineers to support further than only a strict domain. We support all components of our entire platform, regardless if it is a Linux kernel issue like this, involves setting up and maintaining a NoSQL cluster, or helping the business with something like requesting a certificate.&lt;/p&gt;

&lt;p&gt;If you are interested in learning more about this position, take a look at our Vacancy and get in contact with us. &lt;a href="https://werkenbij.vandebron.nl/"&gt;https://werkenbij.vandebron.nl/&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;This post was originally published on &lt;a href="https://vandebron.tech/blog/migrating-dcos-kubernetes-l4lb"&gt;vandebron.tech&lt;/a&gt;. Reposted automatically with &lt;a href="https://reposted.io?utm_source=postFooter"&gt;Reposted.io&lt;/a&gt;. &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Looking back at the Vandebron GreenTech Hackathon 2021</title>
      <dc:creator>Vandebron Technology</dc:creator>
      <pubDate>Tue, 25 Apr 2023 15:19:12 +0000</pubDate>
      <link>https://dev.to/vandebron/looking-back-at-the-vandebron-greentech-hackathon-2021-1lg5</link>
      <guid>https://dev.to/vandebron/looking-back-at-the-vandebron-greentech-hackathon-2021-1lg5</guid>
      <description>&lt;p&gt;At the end of March, we organized a public hackathon to create solutions to tackle climate challenges. After having done internal hackathons, we thought it was time to share our technologies with other innovative people and companies. Together with a group of partners, and enthusiastic participants, spend three full days of (remote) hacking with great results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why organize a public hackathon?
&lt;/h3&gt;

&lt;p&gt;Climate change is one of the many pressing challenges our society is currently facing. At &lt;a href="https://vandebron.nl/"&gt;Vandebron&lt;/a&gt;, we want to continue finding ways to tackle this immense challenge. That’s why we decided to organize a 3-day GreenTech hackathon that ran from March 31st to April 2nd, 2021. We've been organizing internal hackathons for the past four years, to foster innovation within our company and allow our developers to work on something exciting without any constraints. If you want to read more about why we organize internal hackathons, you can find an article by our CTO &lt;a href="https://www.vandebron.tech/blog/power-regular-hackathons"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By organizing a public hackathon, we hoped to attract a bigger audience, possibly even outside our country, The Netherlands, and attract partners to work together with. We succeeded in both, and together with &lt;a href="https://hack-the-planet.io/"&gt;Hack the Planet&lt;/a&gt; and &lt;a href="https://solarracing.nl/"&gt;Top Dutch Solar Racing&lt;/a&gt;, we wanted to find technological solutions to problems in wildlife conservation and renewable energy. For these three days, all participants got the opportunity to work on challenges from our partners, access their technology and knowledge, and got the chance to win unique prizes. Also, we organized a free event with speakers Florian Dirkse (&lt;a href="https://theoceancleanup.com/"&gt;The Ocean Cleanup&lt;/a&gt;), Thijs Suijten (Hack the Planet) and Heleen Klinkert (&lt;a href="https://nieuw-groen.nl/"&gt;Nieuw Groen&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Looking back
&lt;/h3&gt;

&lt;p&gt;The event started on March 31st, when all hackathon challenges were presented and the participants could select which challenge they wanted to work on. People from all over The Netherlands (and even beyond) signed up for the hackathon, ranging from students from the University of Amsterdam to young professionals looking for a job. The first challenge the participants could subscribe to was from Vandebron itself, where teams got the opportunity to use a selection of our Electronic Vehicle (EV) data. With this data, they could for example make a forecast on the amount of charging sessions we could expect on a typical day. Second, our partner Hack the Planet presented their challenge that was aimed at thinking of innovative solutions for their project &lt;a href="https://www.hackthepoacher.com/"&gt;Hack the Poacher&lt;/a&gt;. With Hack the Poacher, they install smart camera traps in African wildlife reservations to detect poachers. The teams could use their camera traps and data to create more solutions to map the poachers or use the camera traps for other needs. Finally, the students from Top Dutch Solar Racing presented a challenge to simulate the race they were supposed to join at the end of the year in Australia. Using their weather and traffic data, the teams could simulate the race and predict how much time they would need to complete the race. After selecting a challenge, all teams started the hackathon and participated in sessions to learn more about the challenges to get started.&lt;/p&gt;

&lt;p&gt;All teams continued working on the hackathon challenge on the second day, after a nice warming-up quiz about climate change in the morning. For most teams this second day was when their project started to take shape, and they got a better idea about what they would be presenting on the final day. This second day was also an opportunity for non-technical people to get to know Vandebron and their partners better as we organized inspirational sessions with talks from different speakers in the afternoon. One of the co-founders from The Ocean Cleanup, Florian Dirkse, inspired us with his story behind making a difference in the world. After which, one of our hackathon partners Thijs Suijten, from Hack the Planet, demonstrated how technology can be used for the good. Our third, and final, speaker Heleen Klinkert (Nieuw Groen), showed how we can compensate for our CO2 emissions by storing them in the soil.&lt;/p&gt;

&lt;p&gt;On the final day of the hackathon, all teams had to finalize their projects and create a presentation for the closing ceremony. During this ceremony, all participants and partners looked back at the past three days and shared what they had been working on during the hackathon. For every challenge, one team could win and take home several prizes, sponsored by &lt;a href="https://marie-stella-maris.com/"&gt;Marie-Stella-Maris&lt;/a&gt;, &lt;a href="https://evexperience.nl/"&gt;EV Experience&lt;/a&gt;, and &lt;a href="https://www.klimaatroute.nl/"&gt;Klimaatroute&lt;/a&gt;. The first presentations were for the Vandebron challenge about EV forecasts. This challenge was won by not one but two teams as the jury and audience were so impressed by their solutions. Both teams created not only the forecast based on the sample data provided, but also created interactive dashboards. On the challenge for Hack the Planet, the team that won came up with a unique solution to use the camera traps to detect wild animals on the streets. For countries like India, this is a huge problem, as wild animals get stuck in traffic or walk through rural areas. The final winner of the hackathon was a group of students that simulated the Top Dutch Solar Racing trip through Australia and forecasted they could complete the race within 7 days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thanks everyone
&lt;/h3&gt;

&lt;p&gt;I'd like to thank all the participants, prize/challenge partners, and speakers for their efforts during these days. The GreenTech Hackathon 2021 was a huge success thanks to everyone that has been involved. Keep following the &lt;a href="https://vandebron.tech"&gt;vandebron.tech&lt;/a&gt; to be updated on future hackathons and events.&lt;/p&gt;




&lt;p&gt;This post was originally published on &lt;a href="https://vandebron.tech/blog/looking-back-at-vandebron-greentech-hackathon-2021"&gt;vandebron.tech&lt;/a&gt;. Reposted automatically with &lt;a href="https://reposted.io?utm_source=postFooter"&gt;Reposted.io&lt;/a&gt;. &lt;/p&gt;

</description>
    </item>
    <item>
      <title>How Vandebron helps balancing the Dutch energy grid together with OnLogic &amp; Talos Linux</title>
      <dc:creator>Tim van Druenen</dc:creator>
      <pubDate>Tue, 25 Apr 2023 09:19:25 +0000</pubDate>
      <link>https://dev.to/vandebron/how-vandebron-helps-balancing-the-dutch-energy-grid-together-with-onlogic-talos-linux-2h83</link>
      <guid>https://dev.to/vandebron/how-vandebron-helps-balancing-the-dutch-energy-grid-together-with-onlogic-talos-linux-2h83</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--y_4Dft0s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dinoo0l18lc58tklpw7b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--y_4Dft0s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dinoo0l18lc58tklpw7b.png" alt="Vandebron curtailment" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vandebron is a Dutch green-tech energy company on a mission to accelerate the transition to 100% renewable energy, 100% of the time. As part of &lt;a href="https://vandebron.nl/100procentgroen"&gt;our mission and strategy&lt;/a&gt;, we are constantly innovating and looking for ways to optimize energy operations and reduce negative impacts when it comes to energy production.&lt;/p&gt;

&lt;p&gt;Our new mission: &lt;a href="https://youtu.be/_Yf8jk4gZbI"&gt;100% renewable energy, 100% of the time&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The importance of curtailment and flexibility services
&lt;/h3&gt;

&lt;p&gt;One area where we are currently focusing our efforts is the area of curtailment and flexibility of wind turbines, solar parks, industrial batteries and electric vehicles. &lt;a href="https://vandebron.nl/blog/curtailment-slimmer-omgaan-met-goeie-energie"&gt;Curtailment&lt;/a&gt; refers to the practice of reducing the electricity inflow to balance the electricity grid. In other words, it involves adjusting the operation of, for example, a wind turbine in order to match the demand for electricity at any given time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vandebron.nl/blog/hoe-houdt-onze-technologie-het-energienet-in-balans"&gt;This is often necessary&lt;/a&gt; because the output of renewable energy sources can vary significantly due to changes in weather conditions. If the output of these sources exceeds the demand for electricity, it can lead to an excess of electricity on the grid, which can cause stability issues. On the other hand, if the output of wind turbines is too low, it can lead to a deficit of electricity on the grid, which can cause blackouts or other disruptions. To tackle this, we look at our customer’s batteries and electric vehicles offering flexibility capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our journey to finding reliable, secure and energy-efficient hardware and software
&lt;/h3&gt;

&lt;p&gt;To optimize these curtailment and flexibility efforts, we were in need of a gateway device that we could place at the installations of the producers on our platform. To keep it close to our mission, we preferred an ARM-based CPU for its &lt;a href="https://www.redhat.com/en/topics/linux/ARM-vs-x86"&gt;energy efficiency&lt;/a&gt; compared to an x86-based CPU. After all, we don’t want to consume all of the produced energy to power an actively cooled NUC… 😉&lt;/p&gt;

&lt;p&gt;While gathering our hardware requirements, we concluded there was really only one competitor. Therefore, we partnered up with OnLogic! We chose their &lt;a href="https://www.onlogic.com/fr201/"&gt;Factor 201 device&lt;/a&gt;, which boasts the ARM-based Raspberry Pi CM4 module packed in a small and beautiful orange industrial chassis. The model also enables a lot of custom configurations. For example, we are able to configure multiple (wireless) networks, add extra SSD storage or optionally mount on DIN rails.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---pQMokGT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/leonj36u8vub89cv2wgy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---pQMokGT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/leonj36u8vub89cv2wgy.jpg" alt="OnLogic Factor 201" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To ensure our gateway devices are secure and agile (like us, developers, 😛) we needed them to integrate well into our existing technology landscape based on Kubernetes. After struggling for some time to harden several (lightweight) operating systems and bootstrapping lightweight Kubernetes clusters our eyes fell on a new kid in town: ‘Talos Linux, the Kubernetes Operating system’ built by &lt;a href="https://www.siderolabs.com/"&gt;Sidero Labs&lt;/a&gt;. Again our predetermined wishlist was covered (even more), and what we got is a minimal OS tailored for Kubernetes, hardened, immutable and ephemeral out-of-the-box. Can you survive even more buzzwords than that? &lt;/p&gt;

&lt;p&gt;Until the present day though, they have fulfilled every promise made on &lt;a href="https://www.talos.dev/"&gt;their website&lt;/a&gt;. It initially didn’t work on our ARM CM4-based device from OnLogic. But after testing a lot together with their team (thank you!) the &lt;a href="https://www.talos.dev/v1.3/introduction/what-is-new/#raspberry-generic-images"&gt;latest release (v1.3.0)&lt;/a&gt; officially supports our ARM devices. Ready for action! Right after the stable release the first batches were shipped and connected to the installations of our producers on the platform.&lt;/p&gt;

&lt;p&gt;Overall, Vandebron's use of OnLogic's fabricated gateway devices running Talos Linux demonstrates the potential of IoT computing to drive innovation and sustainability in the renewable energy industry. By leveraging the power of these technologies combined, we are one step closer to achieving our goal of 100% renewable energy, 100% of the time. Care to join our mission? Look for &lt;a href="https://werkenbij.vandebron.nl/"&gt;open positions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>iot</category>
      <category>curtailment</category>
      <category>arm64</category>
      <category>kubernetes</category>
    </item>
  </channel>
</rss>
