Category: Engineering blogs

  • How to Test the Performance of Flutter Apps – A Step-by-step Guide

    The rendering performance of a mobile app is crucial in ensuring a smooth and delightful user experience. 

    We will explore various ways of measuring the rendering performance of a mobile application, automate this process, and understand the intricacies of rendering performance metrics.

    Internals of Flutter and the Default Performance

    Before we begin exploring performance pitfalls and optimizations, we first need to understand the default performance of a basic hello-world Flutter app. Flutter apps are already highly optimized for speed and are known to perform better than existing cross-platform application development platforms, such as React Native or Apache Cordova.

    By default, Flutter apps aim to render at 60 frames per second on most devices and up to 120 frames per second on devices that support a 120 Hz refresh rate. This is made possible because of Flutter’s unique rendering mechanism.

    It doesn’t render UI components like a traditional mobile application framework, which composes native widgets on the screen. Instead, it uses a high-performance graphics engine, Skia, and renders all components on the screen as if they were part of a single two-dimensional scene.

    Skia is a highly optimized, two-dimensional graphics engine used by a variety of apps, such as Google Chrome, Fuchsia, Chrome OS, Flutter, etc. This game-like rendering behavior gives Flutter an advantage over existing applications SDK when it comes to default performance.

    Common Performance Pitfalls:

    Now, let’s understand some common performance issues or pitfalls seen in mobile applications. Some of them are listed below: 

    • Latency introduced because of network and disk IO 
    • When heavy computations are done on the main UI thread
    • Frequent and unnecessary state updates
    • Jittery UX due to lack of progressive or lazy loading of images and assets
    • Unoptimized or very large assets can take a lot of time to render

    To identify and fix these performance bottlenecks, mobile apps can be instrumented for time complexity and/or space complexity.

    Most of these issues can be identified using a profile. Profiling an app means dynamically analyzing the application’s code, in a runtime environment, for CPU and memory usage and sometimes the usage of other resources, such as network and battery. Performance profiling entails analyzing CPU usage for time complexity to identify parts of the application where CPU usage is high and beyond a certain threshold. Let’s see how profiling works in the Flutter ecosystem.

    How to Profile a Flutter App

    Below are a set of steps that you may follow along to set up profiling on a Flutter app.

    1. Launch the application in profile mode. To do so, we can run the app using the command
    flutter run --profile

    on the terminal or set up a launch configuration for the IDE or code editor. Testing the performance of Flutter apps in profile mode and not in debug (dev) mode ensures that the true release performance of the application is assessed. Dev mode has additional pieces of code running that aren’t part of release builds.

    1. Some developers may need to activate Flutter ‘devtools’ by executing this command: 
    flutter pub global activate devtools

    1. To set up ‘profile mode’, launch the configuration for a Flutter app in VSCode; edit or create the file at project_directory/.vscode/launch.json and create a launch configuration “Profile” as follows:
    {
     "version": "0.2.0",
     "configurations": [
       {
         "name": "Development",
         "request": "launch",
         "type": "dart"
       },
       {
         "name": "Profile",
         "request": "launch",
         "type": "dart",
         "flutterMode": "profile"
       }
     ]
    }

    1. Once the application is running on a real device, go to the timeline view of the DevTools and enable performance overlays. This allows developers to see two graphs on top of each other and overlaid on top of the application. The top graph represents the raster thread timeline, and the second graph below it represents the UI thread timeline. 

    ⚠️ Caution: It is recommended that performance profiling of a Flutter application should only be done on a real device and not on any simulator or emulator. Simulators are not an exact representation of a real device when it comes to hardware and software capabilities, disk IO latency, display refresh rate, etc. Furthermore, the profiling is best done on the slowest, oldest device that the application targets. This ensures that the application is well-tested for performance pitfalls on target platforms and will offer a smooth user experience to end-users.

    Understanding the Performance Overlays

    Once the timeline view is enabled in profile mode, the application’s running instance gets an overlay on the top area. This overlay has two charts on top of each other.

    Both charts display timeline metrics 300 frames at a time. Any frame going over the horizontal black lines on the chart means that the frame is taking more than 16 milliseconds to render, which leads to a frame drop and eventually a jittery user experience.

    Fig:- Dart profiler for optimal rendering

    Look at the timeline above. No frames are going over the black lines,  i.e., no frame takes more than 16 milliseconds to render. This represents an optimal rendering with no frame drops, i.e., no jank for end users.

    Fig:- Dart profiler for suboptimal rendering

    Here, some frames in the timeline above are going over the horizontal black lines, i.e., some frames are taking more than 16 milliseconds to render. That is because the application was trying to load an image from the network while the user was also scrolling through the page. This means there is some performance bottleneck in this part of the application, which can be further optimized to ensure smoother rendering, i.e., a jank-free end-user experience.

    The two graphs mentioned above can be described as:

    1. UI thread: This is the first chart, and it portrays the timeline view of all the dart code executions. Instructions written by developers are executed on this thread, and a layer tree (for rendering) is created, which is then sent to the raster thread for rendering. 
    2. Raster thread: The raster thread runs the Skia engine and talks to the GPU and is responsible for drawing the screen’s layer tree. Developers can not directly instruct the GPU thread. Most performance optimizations are applicable to the UI thread because the raster thread is already optimized by the Flutter dev team.

    Automatically Testing for Jank:

    Profiling the app gives some idea of which screens and user interaction may be optimized for performance, but it doesn’t actually give a concrete reproducible assessment. So, let’s write some code to automate the process of profiling and detecting sources of lag in our Flutter app.

    First, include the Flutter driver extension in the application’s main entrypoint file and enable the Flutter drive extension. In most cases, this file is called main.dart and invokes the runApp() method.

    import 'package:flutter_driver/driver_extension.dart';
     
    void main() {
      enableFlutterDriverExtension();
      runApp(MyApp());
    }

    Next, let’s write a Flutter driver script to drive parts of the application that need to be profiled. Any and all user behavior such as navigation, taps, scroll, multipoint touches, and gestures can be simulated by a driver script.

    To measure the app’s rendering performance, we will make sure that we are driving and testing parts of the application exactly like a user would do, i.e., we need to test interactions like click or scroll and transitions like page changes and back navigation. Flutter driver makes this simpler by introducing a huge set of methods such as find(), tap(), scroll(), etc.

    The driver script will also have to account for and mock any sources of latency, such as time taken during API calls or while reading a file from the local file system.

    We also need to run these automated tests multiple times to draw conclusions from average render times.

    The following test driver script checks for a simple user interaction:

    • Launches the app
    • Waits for a list of items
    • Finds and clicks on the first list item, which takes users to a different page
    • Views some information on the page
    • Presses the back button to go back to the list

    The script also does the following:

    • Tracks time taken during each user interaction by wrapping interactions inside the driver.traceAction() method
    • Records and writes the UI thread and the raster thread timelines to a file ui_timeline.json
    import 'package:flutter_driver/flutter_driver.dart';
    import 'package:test/test.dart';
     
    void main() {
     group('App name - home', () {
       FlutterDriver driver;
     
       setUpAll(() async {
         driver = await FlutterDriver.connect();
       });
     
       tearDownAll(() async {
         if (driver != null) {
           driver.close();
         }
       });
     
       test('list has row items', () async {
         final timeline = await driver.traceAction(() async {
           // wait for list items
           await driver.waitFor(find.byValueKey('placesList'));
     
           // get the first row in the list
           final firstRow = find.descendant(
               of: find.byValueKey('placesList'),
               matching: find.byType('PlaceRow'),
               firstMatchOnly: true);
     
           // tap on the first row
           await driver.tap(firstRow);
     
           // wait for place details
           await driver.waitFor(find.byValueKey("placeDetails"));
     
           // go back to lists
           await driver.tap(find.byTooltip('Back'));
         });
     
         // write summary to a file
         final summary = new TimelineSummary.summarize(timeline);
         await summary.writeSummaryToFile('ui_timeline', pretty: true);
         await summary.writeTimelineToFile('ui_timeline', pretty: true);
       });
     });

    To run the script, the following command can be executed on the terminal:

    flutter drive -t lib/main.dart --driver test_driver/main_test.dart --profile

    The test driver creates a release-like app bundle that is installed on the target device and driven by the driver script. This test is recommended to be run on a real device, preferably the slowest device targeted by the app.

    Once the script finishes execution, two json files are written to the build directory.

    ./build/ui_timeline.timeline_summary.json
    ./build/ui_timeline.timeline.json

    Viewing the Results:

    Launch the Google Chrome web browser and go to URL: chrome://tracing. Click on the load button on the top left and load the file ui_timeline.timeline.json.

    The timeline summary when loaded into the tracing tool can be used to walk through the hierarchical timeline of the application and exposes various metrics, such as CPU duration, start time, etc., to better understand sources of performance issues in the app. The tracing tool is versatile and displays methods invoked under the hood in a hierarchical view that can be navigated through by mouse or by pressing A, S, D, F keys. 

    Fig:- Chrome tracing in action

    The other file, i.e., the timeline_summary file, can be opened in a code editor and eye-balled for performance data. It provides a set of metrics related to the performance of the application. For example, the flutter_driver script above outputs the following timeline on a single run:

    "average_frame_build_time_millis": 1.6940195121951216,
     "90th_percentile_frame_build_time_millis": 2.678,
     "99th_percentile_frame_build_time_millis": 7.538,
     "worst_frame_build_time_millis": 14.687,
     "missed_frame_build_budget_count": 0,
     "average_frame_rasterizer_time_millis": 6.147395121951226,
     "90th_percentile_frame_rasterizer_time_millis": 9.029,
     "99th_percentile_frame_rasterizer_time_millis": 15.961,
     "worst_frame_rasterizer_time_millis": 21.476,
     "missed_frame_rasterizer_budget_count": 2,
     "frame_count": 205,
     "frame_rasterizer_count": 205,
     "average_vsync_transitions_missed": 1.5,
     "90th_percentile_vsync_transitions_missed": 2.0,
     "99th_percentile_vsync_transitions_missed": 2.0
    }

    Each of these metrics can be inspected, analyzed, and optimized. For example, the value of average_frame_build_time_millis should always be below 16 milliseconds to ensure that the app runs at 60 frames per second. 

    More details about each of these fields can be found here.

    Conclusion

    In this blog post, we explored how to profile and measure the performance of a Flutter application. We also explored ways to identify and fix performance pitfalls, if any.

    We then created a Flutter driver script to automate performance testing of Flutter apps and produce a summary of rendering timelines as well as various performance metrics such as average_frame_build_time_millis.

    The automated performance tests ensure that the app is tested for performance in a reproducible way against different devices and can be run multiple times to calculate a running average and draw various insights. These metrics can be objectively looked at to measure the performance of an application and fix any bottlenecks in the application.

    A performant app means faster rendering and optimal resource utilization, which is essential to ensuring a jank-free and smooth user experience. It also contributes greatly to an app’s popularity. Do try profiling and analyzing the performance of some of your Flutter apps!

    Related Articles

    1. A Primer To Flutter

    2. Building High-performance Apps: A Checklist To Get It Right

  • Serverless Computing: Predictions for 2017

    Serverless is the emerging trend in software architecture. 2016 was a very exciting year for serverless and adoption will continue to explode in 2017. This post covers the interesting developments in serverless space in 2016 and my thoughts on how this space will evolve in 2017.

    What is serverless?

    In my opinion, Serverless means two things:
    1. Serverless was initially used to describe fully hosted services or Backend-as-a-Service (BaaS) offerings where you fully depend on a 3rd party to manage the server-side logic and state. Examples include AWS S3(storage), Auth0(authentication), AWS Dynamo (database), Firebase, etc. 
    2. The popular interpretation of Serverless is Functions-as-a-Service(FaaS) where developers can upload code that is run within stateless compute containers that are triggered by a variety of events, are ephemeral and fully managed by the cloud platform. FaaS obviates the need to provision, manage, scale or manage availability of your own servers. The most popular FaaS offering is AWS Lambda but Microsoft Azure Functions , Auth0 Webtask, Google Cloud Functions are also coming up with fast maturing offerings. IBM OpenWhisk, Iron.io allow serverless environments to be setup on-premise as well.

    This post will focus more on FaaS and especially AWS Lambda since it is the leader in this space with the largest feature set.

    Serverless vs PaaS

    People are comparing serverless and Platform-as-a-Service(PaaS) which is an invalid comparison in my opinion. PaaS platforms provide you the ability to deploy and manage your entire application or micro-service. PaaS platforms will help you to scale your application infrastructure. Anyone who has used a PaaS knows that while it reduces administration, it does not do away with it. PaaS does not really require you to re-evaluate your code design. In PaaS, your application needs to be always on, though it can be scaled up or down.

    Serverless on the other hand is about “breaking up your application into discrete functions to obviate the need for any complex infrastructure or it’s management”. Serverless has several restrictions with respect to execution duration limits and state. Serverless requires developers to architect differently thinking about discrete functionality of each component of your application.

    New features from AWS for Serverless

    1. Lambda@Edge: This feature allows Lambda code to be executed on global AWS Edge locations. Imagine you deploy your code to one region of AWS and then are able to run it in any of the 10s and soon 100s of AWS Edge locations around the globe. Imagine the reduction in network latency for your end users.
    2. Step Functions: Companies that adopt serverless  soon end up with 10s of 100s of functions. The logic and workflow between these functions is hard to track and manage. Step Functions allow developers to create visual workflows to organize the various components, micro-services, events and conditions. This is nothing but a Rapid Application Development(RAD) product. I expect AWS to build a fully functional enterprise RAD based on this feature.
    3. API Gateway Monetisation: This is big deal in my opinion. There are various trends like a) startups are increasingly API-first b) all enterprises and startups build 10s or 100s of integrations with APIs c) fine-grained usage based billing based on API usage d) adoption of micro-services architecture which uses API contracts. This feature allows companies to start monetising their APIs via the AWS Marketplace. And the APIs can be implemented in AWS Lambda in the backend. I expect to see a lot of “data integration”, “data pipeline” and “data marketplace” companies try out this approach.
    4. AWS Greengrass: Extend AWS compute to devices. This feature enables running Lambda functions offline on devices in the field. This is another great feature which is extending the meaning of a “global cloud”. Most of the use-cases for this feature are in IoT space.
    5. Continuous Deployment for Serverless: AWS CodePipeline, CodeCommit and CodeBuild support AWS Lambda and enable creation of continuous integration and deployment pipelines. Jenkins and other CI tools also have some plugins for serverlesss which are improving all the time.
    6. API Gateway Developer Portal: Easily build your own developer portal with API documentation. Support as per Swagger documentation.
    7. AWS X-Ray: Analyze and debug distributed applications, commonly used with micro-services architecture. X-Ray will soon get Lambda support and enable even easier debugging of Lambda logic flows across all the AWS services and triggers that are part of your workflow.

    Predictions

    • Your code will increasingly run closer to clients. With Lambda@Edge, Greengrass & Snowball Edge, you can truly “deploy once and run around the globe”. This is not just scaling horizontally but scaling up or down geographically. I expect customers to leverage this feature in some very interesting ways.
    • Serverless frameworks will mature and allow easy creation of simple REST applications. I also expect vertical specific serverless frameworks to evolve, especially for IoT.
    • Monitoring, logging and debugging for serverless will improve with cloud vendor solutions as well as frameworks providing capabilities. A good example is IOPipe which provides a monitoring and logging capabilities by instrumenting your Lambda code.
    • I expect all the continuous integration and deployment tool vendors to add increasing support for serverless architectures in 2017. Automated testing for serverless will become easier via frameworks and CI tools.
    • With CloudFormation and Step Functions, AWS will try and solve the versioning and discovery problem for Lambda functions.
    • Mature patterns will emerge for serverless usage. For example, some very common use-cases today are image or video conversion based on S3 trigger, backends for messaging bots, data processing for IoT, etc. I expect to see more mature patterns and use-cases where serverless becomes an obvious solution. Expect lots of interesting white-papers and case studies from AWS to educate the market on merging use-cases.
    • AWS will allow users to choose to keep some number of Lambda instances always-on for lower latency with some extra pricing. This will start addressing the latency issues that some heavy functions or frameworks entail.
    • API and data integration vendors will increasing use Lambda and monetized API gateways. An early example is Cloud Elements which has released a feature that allows publishing SaaS APIs as AWS Lambda functions.

    Serverless Frameworks

    There are open-source projects like ServerlessChaliceLambda FrameworkApex, Gomix which add a level of abstraction over vendor serverless platforms. These frameworks make it easier to develop and deploy serverless components.

    Some of these frameworks plan to allow abstraction across FaaS offerings to avoid lock-in into a single platform. I do not expect to see any abstraction or standardisation that allows portability of serverless functions. Cloud vendors have unique FaaS offerings with different implementations and the triggers/events are based on their own proprietary services (for example, AWS has S3, Kinesis, DynamodDB, etc. triggers).

    Bots, IoT, mobile app backends, IoT backends, data processing/ETL, scheduled jobs and any other event driven use-cases are a perfect fit for serverless.

    Conclusion

    In my opinion, any service that adds agility along with reduction in IT operations and costs will become successful. Serverless definitely fits into this philosophy and in my opinion, will continue to see increasing adoption. It will not “replace” existing architectures but augment them. The serverless movement is just getting started and you can expect cloud vendors to invest heavily in improving the feature set and capabilities of serverless offerings. The serverless frameworks, DevOps teams, operational management and monitoring of FaaS will continue to mature in 2017. There are a lot of emergent trends at play here — containerization, serverless, LessOps, voice and chatbots, IoT proliferation, globalization and agility demands. All of these will accelerate serverless adoption.

  • Understanding Node.js Async Flows: Parallel, Serial, Waterfall and Queues

    Promises in Javascript has been around for a long time now. It helped solve the problem of callback hell. But as soon as the requirements get complicated with control flows, promises start getting unmanageable and harder to work with. This is where async flows come to the rescue. In this blog, let’s talk about the various async flows which are used frequently rather than raw promises and callbacks.

    Async Utility Module

    Async is a utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript. Although it is built on top of promises, it makes asynchronous code look and behave a little more like synchronous code, making it easier to read and maintain.

    Async utility has a number of control flows. Let’s discuss the most popular ones and their use cases:

    1. Parallel

    When we have to run multiple tasks independent of each other without waiting until the previous task has completed, parallel comes into the picture.

    async.parallel(tasks, callback)

    Tasks: A collection of functions to run. It can be an array, an object or any iterable.

    Callback: This is the callback where all the task results are passed and is executed once all the task execution has completed.

    In case an error is passed to a function’s callback, the main callback is immediately called with the error. Although parallel is about starting I/O tasks in parallel, it’s not about parallel execution since Javascript is single-threaded.

    An example of Parallel is shared below:

    async.parallel([
      function(callback) {
        setTimeout(function() {
          console.log('Task One');
          callback(null, 1);
        }, 200);
      },
      function(callback) {
        setTimeout(function() {
          console.log('Task Two');
          callback(null, 2);
        }, 100);
      }
    ],
    function(err, results) {
      console.log(results);
      // the results array will equal [1, 2] even though
      // the second function had a shorter timeout.
    });
    
    // an example using an object instead of an array
    async.parallel({
      task1: function(callback) {
        setTimeout(function() {
          console.log('Task One');
          callback(null, 1);
        }, 200);
      },
      task2: function(callback) {
        setTimeout(function() {
          console.log('Task Two');
          callback(null, 2);
        }, 100);
        }
    }, function(err, results) {
      console.log(results);
      // results now equals to: { task1: 1, task2: 2 }
    });

    2. Series

    When we have to run multiple tasks which depend on the output of the previous task, series comes to our rescue.

    async.series(tasks, callback)

    Tasks: A collection of functions to run. It can be an array, an object or any iterable.

    Callback: This is the callback where all the task results are passed and is executed once all the task execution has completed.

    Callback function receives an array of result objects when all the tasks have been completed. If an error is encountered in any of the task, no more functions are run but the final callback is called with the error value.

    An example of Series is shared below:

    async.series([
      function(callback) {
        console.log('one');
        callback(null, 1);
      },
      function(callback) {
        console.log('two');
        callback(null, 2);
      },
      function(callback) {
        console.log('three');
        callback(null, 3);
      }
    ],
    function(err, results) {
      console.log(result);
      // results is now equal to [1, 2, 3]
    });
    
    async.series({
      1: function(callback) {
        setTimeout(function() {
          console.log('Task 1');
          callback(null, 'one');
        }, 200);
      },
      2: function(callback) {
        setTimeout(function() {
          console.log('Task 2');
          callback(null, 'two');
        }, 300);
      },
      3: function(callback) {
        setTimeout(function() {
          console.log('Task 3');
          callback(null, 'three');
        }, 100);
      }
    },
    function(err, results) {
      console.log(results);
      // results is now equal to: { 1: 'one', 2: 'two', 3:'three' }
    });

    3. Waterfall

    When we have to run multiple tasks which depend on the output of previous task, Waterfall can be helpful.

    async.waterfall(tasks, callback)

    Tasks: A collection of functions to run. It can be an array, an object or any iterable structure.

    Callback: This is the callback where all the task results are passed and is executed once all the task execution has completed.

    It will run one function at a time and pass the result of the previous function to the next one.

    An example of Waterfall is shared below:

    async.waterfall([
      function(callback) {
        callback(null, 'Task 1', 'Task 2');
      },
      function(arg1, arg2, callback) {
        // arg1 now equals 'Task 1' and arg2 now equals 'Task 2'
        let arg3 = arg1 + ' and ' + arg2;
        callback(null, arg3);
      },
      function(arg1, callback) {
        // arg1 now equals 'Task1 and Task2'
        arg1 += ' completed';
        callback(null, arg1);
      }
    ], function(err, result) {
      // result now equals to 'Task1 and Task2 completed'
      console.log(result);
    });
    
    // Or, with named functions:
    async.waterfall([
      myFirstFunction,
      mySecondFunction,
      myLastFunction,
    ], function(err, result) {
      // result now equals 'Task1 and Task2 completed'
      console.log(result);
    });
    
    function myFirstFunction(callback) {
      callback(null, 'Task 1', 'Task 2');
    }
    function mySecondFunction(arg1, arg2, callback) {
      // arg1 now equals 'Task 1' and arg2 now equals 'Task 2'
      let arg3 = arg1 + ' and ' + arg2;
      callback(null, arg3);
    }
    function myLastFunction(arg1, callback) {
      // arg1 now equals 'Task1 and Task2'
      arg1 += ' completed';
      callback(null, arg1);
    }

    4. Queue

    When we need to run a set of tasks asynchronously, queue can be used. A queue object based on an asynchronous function can be created which is passed as worker.

    async.queue(task, concurrency)

    Task: Here, it takes two parameters, first – the task to be performed and second – the callback function.

    Concurrency: It is the number of functions to be run in parallel.

    async.queue returns a queue object that supports few properties:

    • push: Adds tasks to the queue to be processed.
    • drain: The drain function is called after the last task of the queue.
    • unshift: Adds tasks in front of the queue.

    An example of Queue is shared below:

    // create a queue object with concurrency 2
    var q = async.queue(function(task, callback) {
      console.log('Hello ' + task.name);
      callback();
    }, 2);
    
    // assign a callback
    q.drain = function() {
      console.log('All items have been processed');
    };
    
    // add some items to the queue
    q.push({name: 'foo'}, function(err) {
      console.log('Finished processing foo');
    });
    
    q.push({name: 'bar'}, function (err) {
      console.log('Finished processing bar');
    });
    
    // add some items to the queue (batch-wise)
    q.push([{name: 'baz'},{name: 'bay'},{name: 'bax'}], function(err) {
      console.log('Finished processing item');
    });
    
    // add some items to the front of the queue
    q.unshift({name: 'bar'}, function (err) {
      console.log('Finished processing bar');
    });

    5. Priority Queue

    It is the same as queue, the only difference being that a priority can be assigned to the tasks which is considered in ascending order.

    async.priorityQueue(task,concurrency)

    Task: Here, it takes three parameters:

    • First – task to be performed.
    • Second – priority, it is a number that determines the sequence of execution. For array of tasks, the priority remains same for all of them.
    • Third – Callback function.

    The async.priorityQueue does not support ‘unshift’ property of the queue.

    An example of Priority Queue is shared below:

    // create a queue object with concurrency 1
    var q = async.priorityQueue(function(task, callback) {
      console.log('Hello ' + task.name);
      callback();
    }, 1);
    
    // assign a callback
    q.drain = function() {
      console.log('All items have been processed');
    };
    
    // add some items to the queue with priority
    q.push({name: 'foo'}, 3, function(err) {
      console.log('Finished processing foo');
    });
    
    q.push({name: 'bar'}, 2, function (err) {
      console.log('Finished processing bar');
    });
    
    // add some items to the queue (batch-wise) which will have same priority
    q.push([{name: 'baz'},{name: 'bay'},{name: 'bax'}], 1, function(err) {
      console.log('Finished processing item');
    });

    6. Race

    It runs all the tasks in parallel, but as soon as any of the function completes its execution or passes error to its callback, the main callback is immediately called.

    async.race(tasks, callback)

    Task: Here, it is a collection of functions to run. It is an array or any iterable.

    Callback: The result of the first complete execution is passed. It may be the result or error.

    An example of Race is shared below:

    async.race([
      function (callback) {
        setTimeout(function () {
          callback(null, 'one');
        }, 300);
      },
      function (callback) {
        setTimeout(function () {
          callback(null, 'two');
        }, 100);
      },
      function (callback) {
        setTimeout(function () {
          callback(null, 'three');
        }, 200);
      }
    ],
      // main callback
      function (err, result) {
        // the result will be equal to 'two' as it finishes earlier than the other 2
        console.log('The result is ', result);
      });

    Combining Async Flows

    In complex scenarios, the async flows like parallel and series can be combined and nested. This helps in achieving the expected output with the benefits of async utilities.

    However, the only difference between Waterfall and Series async utility is that the final callback in series receives an array of results of all the task whereas in Waterfall, the result object of the final task is received by the final callback.

    Conclusion

    Async Utilities has an upper hand over promises due to its concise and clean code, better error handling and easier debugging. It makes us realize how simple and easy asynchronous code can be without the syntactical mess of promises and callback hell.

  • Building Google Photos Alternative Using AWS Serverless

    Being an avid Google Photos user, I really love some of its features, such as album, face search, and unlimited storage. However, when Google announced the end of unlimited storage on June 1st, 2021, I started thinking about how I could create a cheaper solution that would meet my photo backup requirement.

    “Taking an image, freezing a moment, reveals how rich reality truly is.”

    – Anonymous

    Google offers 100 GB of storage for 130 INR. This storage can be used across various Google applications. However, I don’t use all the space in one go. For me, I snap photos randomly. Sometimes, I visit places and take random snaps with my DSLR and smartphone. So, in general, I upload approximately 200 photos monthly. The size of these photos varies in the range of 4MB to 30MB. On average, I may be using 4GB of monthly storage for backup on my external hard drive to keep raw photos, even the bad ones. Photos backed up on the cloud should be visually high-quality, and it’s good to have a raw copy available at the same time, so that you may do some lightroom changes (although I never touch them 😛). So, here is my minimal requirement:

    • Should support social authentication (Google sign-in preferred).
    • Photos should be stored securely in raw format.
    • Storage should be scaled with usage.
    • Uploading and downloading photos should be easy.
    • Web view for preview would be a plus.
    • Should have almost no operations headache and solution should be as cheap as possible 😉.

    Selecting Tech Stack

    To avoid operation headaches with servers going down, scaling, or maybe application crashing and overall monitoring, I opted for a serverless solution with AWS. The AWS S3 is infinite scalable storage and you only pay for the amount of storage you used. On top of that, you can opt for the S3 storage class, which is efficient and cost-effective.

    – Infrastructure Stack

    1. AWS API Gateway (http api)
    2. AWS Lambda (for processing images and API gateway queries)
    3. Dynamodb (for storing image metadata)
    4. AWS Cognito (for authentication)
    5. AWS S3 Bucket (for storage and web application hosting)
    6. AWS Certificate Manager (to use SSL certificate for a custom domain with API gateway)

    – Software Stack

    1. NodeJS
    2. ReactJS and Material-UI (front-end framework and UI)
    3. AWS Amplify (for simplifying auth flow with cognito)
    4. Sharp (high-speed nodejs library for converting images)
    5. Express and serversless-http
    6. Infinite Scroller (for gallery view)
    7. Serverless Framework (for ease of deployment and Infrastructure as Code)

    Create S3 Buckets:

    We will create three S3 buckets. Create one for hosting a frontend application (refer to architecture diagram, more on this discussed later in the build and hosting part). The second one is for temporarily uploading images. The third one is for actual backup and storage (enable server-side encryption on this bucket). A temporary upload bucket will process uploaded images. 

    During pre-processing, we will resize the original image into two different sizes. One is for thumbnail purposes (400px width), another one is for viewing purposes, but with reduced quality (webp format). Once images are resized, upload all three (raw, thumbnail, and webview) to the third S3 bucket and create a record in dynamodb. Set up object expiry policy on the temporary bucket for 1 day. This way, uploaded objects are automatically deleted from the temporary bucket.

    Setup trigger on the temporary bucket for uploaded images:

    We will need to set up an S3 PUT event, which will trigger our Lambda function to download and process images. We will filter the suffix jpg (and jpeg) for an event trigger, meaning that any file with extension .jpg and .jpeg uploaded to our temporary bucket will automatically invoke a lambda function with the event payload. The lambda function with the help of the event payload will download the uploaded file and perform processing. Your serverless function definition would look like:

    functions:
     lambda:
       handler: index.handler
       memorySize: 512
       timeout: 60
       layers:
         - {Ref: PhotoParserLibsLambdaLayer}
       events:
         - s3:
             bucket: your-temporary-bucket-name
             event: s3:ObjectCreated:*
             rules:
               - suffix: .jpg
             existing: true
         - s3:
             bucket: your-temporary-bucket-name
             event: s3:ObjectCreated:*
             rules:
               - suffix: .jpeg
             existing: true

    Notice that in the YAML events section, we set “existing:true”. This ensures that the bucket will not be created during the serverless deployment. However, if you plan not to manually create your s3 bucket, you can let the framework create a bucket for you.

    DynamoDB as metadatadb:

    AWS dynamodb is a key-value document db that is suitable for our use case. Dynamodb will help us retrieve the list of photos available in the time series. Dynamodb uses a primary key for uniquely identifying each record. A primary key can be composed of a hash key and range key (also called a sort key). A range key is optional. We will use a federated identity ID (discussed in setup authorization) as the hash key (partition key) and name it the username for attribute definition with the type string. We will use the timestamp attribute definition name as a range key with a type number. Range key will help us query results with time-series (Unix epoch). We can also use dynamodb secondary indexes to sort results more specifically. However, to keep the application simple, we’re going to opt-out of this feature for now. Your serverless resource definition would look like:

    resources:
     Resources:
       MetaDataDB:
         Type: AWS::DynamoDB::Table
         Properties:
           TableName: your-dynamodb-table-name
           AttributeDefinitions:
             - AttributeName: username
               AttributeType: S
             - AttributeName: timestamp
               AttributeType: N
           KeySchema:
             - AttributeName: username
               KeyType: HASH
             - AttributeName: timestamp
               KeyType: RANGE
           BillingMode: PAY_PER_REQUEST

    Finally, you also need to set up the IAM role so that the process image lambda function would have access to the S3 bucket and dynamodb. Here is the serverless definition for the IAM role.

    # you can add statements to the Lambda function's IAM Role here
     iam:
       role:
         statements:
         - Effect: "Allow"
           Action:
             - "s3:ListBucket"
           Resource:
             - arn:aws:s3:::your-temporary-bucket-name
             - arn:aws:s3:::your-actual-photo-bucket-name
         - Effect: "Allow"
           Action:
             - "s3:GetObject"
             - "s3:DeleteObject"
           Resource: arn:aws:s3:::your-temporary-bucket-name/*
         - Effect: "Allow"
           Action:
             - "s3:PutObject"
           Resource: arn:aws:s3:::your-actual-photo-bucket-name/*
         - Effect: "Allow"
           Action:
             - "dynamodb:PutItem"
           Resource:
             - Fn::GetAtt: [ MetaDataDB, Arn ]

    Setup Authentication:

    Okay, to set up a Cognito user pool, head to the Cognito console and create a user pool with below config:

    1. Pool Name: photobucket-users

    2. How do you want your end-users to sign in?

    • Select: Email Address or Phone Number
    • Select: Allow Email Addresses
    • Check: (Recommended) Enable case insensitivity for username input

    3. Which standard attributes are required?

    • email

    4. Keep the defaults for “Policies”

    5. MFA and Verification:

    • I opted to manually reset the password for each user (since this is internal app)
    • Disabled user verification

    6. Keep the default for Message Customizations, tags, and devices.

    7. App Clients :

    • App client name: myappclient
    • Let the refresh token, access token, and id token be default
    • Check all “Auth flow configurations”
    • Check enable token revocation

    8. Skip Triggers

    9. Review and create the pool

    Once created, goto app integration -> domain name. Create a domain Cognito subdomain of your choice and note this. Next, I plan to use the Google sign-in feature with Cognito Federation Identity Providers. Use this guide to set up a Google social identity with Cognito.

    Setup Authorization:

    Once the user identity is verified, we need to allow them to access the s3 bucket with limited permissions. Head to the Cognito console, select federated identities, and create a new identity pool. Follow these steps to configure:

    1. Identity pool name: photobucket_auth

    2. Keep Unauthenticated and Authentication flow settings unchecked.

    3. Authentication providers:

    • User Pool I: Enter the user pool ID obtained during authentication setup
    • App Client I: Enter the app client ID generated during the authentication setup. (Cognito user pool -> App Clients -> App client ID)

    4. Setup permissions:

    • Expand view details (Role Summary)
    • For authenticated identities: edit policy document and use the below JSON policy and skip unauthenticated identities with the default configuration.
    {
       "Version": "2012-10-17",
       "Statement": [
           {
               "Effect": "Allow",
               "Action": [
                   "mobileanalytics:PutEvents",
                   "cognito-sync:*",
                   "cognito-identity:*"
               ],
               "Resource": [
                   "*"
               ]
           },
           {
               "Sid": "ListYourObjects",
               "Effect": "Allow",
               "Action": "s3:ListBucket",
               "Resource": [
                   "arn:aws:s3:::your-actual-photo-bucket-name"
               ],
               "Condition": {
                   "StringLike": {
                       "s3:prefix": [
                           "${cognito-identity.amazonaws.com:sub}/",
                           "${cognito-identity.amazonaws.com:sub}/*"
                       ]
                   }
               }
           },
           {
               "Sid": "ReadYourObjects",
               "Effect": "Allow",
               "Action": [
                   "s3:GetObject"
               ],
               "Resource": [
                   "arn:aws:s3:::your-actual-photo-bucket-name/${cognito-identity.amazonaws.com:sub}",
                   "arn:aws:s3:::your-actual-photo-bucket-name/${cognito-identity.amazonaws.com:sub}/*"
               ]
           }
       ]
    }

    ${cognito-identity.amazonaws.com:sub} is a special AWS variable. When a user is authenticated with a federated identity, each user is assigned a unique identity. What the above policy means is that any user who is authenticated should have access to objects prefixed by their own identity ID. This is how we intend users to gain authorization in a limited area within the S3 bucket.

    Copy the Identity Pool ID (from sample code section). You will need this in your backend to get the identity id of the authenticated user via JWT token.

    Amplify configuration for the frontend UI sign-in:

    This object helps you set up the minimal configuration for your application. This is all that we need to sign in via Cognito and access the S3 photo bucket.

    const awsconfig = {
       Auth : {
           identityPoolId: "idenity pool id created during authorization setup",
           region : "your aws region",
           identityPoolRegion: "same as above if cognito is in same region",
           userPoolId : "cognito user pool id created during authentication setup",
           userPoolWebClientId : "cognito app client id",
           cookieStorage : {
               domain : "https://your-app-domain-name", //this is very important
               secure: true
           },
           oauth: {
               domain : "{cognito domain name}.auth.{cognito region name}.amazoncognito.com",
               scope : ["profile","email","openid"],
               redirectSignIn: 'https://your-app-domain-name',
               redirectSignOut: 'https://your-app-domain-name',
               responseType : "token"
           }
       },
       Storage: {
           AWSS3 : {
               bucket: "your-actual-bucket-name",
               region: "region-of-your-bucket"
           }
       }
    };
    export default awsconfig;

    You can then use the below code to configure and sign in via social authentication.

    import Amplify, {Auth} from 'aws-amplify';
    import awsconfig from './aws-config';
    Amplify.configure(awsconfig);
    //once the amplify is configured you can use below call with onClick event of buttons or any other visual component to sign in.
    //Example
    <Button startIcon={<img alt="Sigin in With Google" src={logo} />} fullWidth variant="outlined" color="primary" onClick={() => Auth.federatedSignIn({provider: 'Google'})}>
       Sign in with Google
    </Button>

    Gallery View:

    When the application is loaded, we use the PhotoGallery component to load photos and view thumbnails on-page. The Photogallery component is a wrapper around the InfinityScoller component, which keeps loading images as the user scrolls. The idea here is that we query a max of 10 images in one go. Our backend returns a list of 10 images (just the map and metadata to the S3 bucket). We must load these images from the S3 bucket and then show thumbnails on-screen as a gallery view. When the user reaches the bottom of the screen or there is empty space left, the InfiniteScroller component loads 10 more images. This continues untill our backend replies with a stop marker.

    The key point here is that we need to send the JWT Token as a header to our backend service via an ajax call. The JWT Token is obtained post a sign-in from Amplify framework. An example of obtaininga JWT token:

    let authsession = await Auth.currentSession();
    let jwtToken = authsession.getIdToken().jwtToken;
    let photoList = await axios.get(url,{
       headers : {
           Authorization: jwtToken
       },
       responseType : "json"
    });

    An example of an infinite scroller component usage is given below. Note that “gallery” is JSX composed array of photo thumbnails. The “loadMore” method calls our ajax function to the server-side backend and updates the “gallery” variable and sets the “hasMore” variable to true/false so that the infinite scroller component can stop queering when there are no photos left to display on the screen.

    <InfiniteScroll
       loadMore={this.fetchPhotos}
       hasMore={this.state.hasMore}
       loader={<div style={{padding:"70px"}} key={0}><LinearProgress color="secondary" /></div>}
    >
       <div style={{ marginTop: "80px", position: "relative", textAlign: "center" }}>
           <div className="image-grid" style={{ marginTop: "30px" }}>
               {gallery}
           </div>
           {this.state.openLightBox ?
           <LightBox src={this.state.lightBoxImg} callback={this.closeLightBox} />
           : null}
       </div>
    </InfiniteScroll>

    The Lightbox component gives a zoom effect to the thumbnail. When the thumbnail is clicked, a higher resolution picture (webp version) is downloaded from the S3 bucket and shown on the screen. We use a storage object from the Amplify library. Downloaded content is a blob and must be converted into image data. To do so, we use the javascript native method, createObjectURL. Below is the sample code that downloads the object from the s3 bucket and then converts it into a viewable image for the HTML IMG tag.

    thumbClick = (index) => {
       const urlCreater = window.URL || window.webkitURL;
       try {
           this.setState({
               openLightBox: true
           });
           Storage.get(this.state.photoList[index].src,{download: true}).then(data=>{
               let image = urlCreater.createObjectURL(data.Body);
               this.setState({
                   lightBoxImg : image
               });
           });
              
       } catch (error) {
           console.log(error);
           this.setState({
               openLightBox: false,
               lightBoxImg : null
           });
       }
    };

    Uploading Photos:

    The S3 SDK lets you generate a pre-signed POST URL. Anyone who gets this URL will be able to upload objects to the S3 bucket directly without needing credentials. Of course, we can actually set up some boundaries, like a max object size, key of the uploaded object, etc. Refer to this AWS blog for more on pre-signed URLs. Here is the sample code to generate a pre-signed URL.

    let s3Params = {
       Bucket: "your-temporary-bucket-name,
       Conditions : [
           ["content-length-range",1,31457280]
       ],
       Fields : {
           key: "path/to/your/object"
       },
       Expires: 300 //in seconds
    };
    const s3 = new S3({region : process.env.AWSREGION });
    s3.createPresignedPost(s3Params)

    For a better UX, we can allow our users to upload more than one photo at a time. However, a pre-signed URL lets you upload a single object at a time. To overcome this, we generate multiple pre-signed URLs. Initially, we send a request to our backend asking to upload photos with expected keys. This request is originated once the user selects photos to upload. Our backend then generates pre-signed URLs for us. Our frontend React app then provides the illusion that all photos are being uploaded as a whole.

    When the upload is successful, the S3 PUT event is triggered, which we discussed earlier. The complete flow of the application is given in a sequence diagram. You can find the complete source code here in my GitHub repository.

    React Build Steps and Hosting:

    The ideal way to build the react app is to execute an npm run build. However, we take a slightly different approach. We are not using the S3 static website for serving frontend UI. For one reason, S3 static websites are non-SSL unless we use CloudFront. Therefore, we will make the API gateway our application’s entry point. Thus, the UI will also be served from the API gateway. However, we want to reduce calls made to the API gateway. For this reason, we will only deliver the index.html file hosted with the help API gateway/Lamda, and the rest of the static files (react supporting JS files) from S3 bucket.

    Your index.html should have all the reference paths pointed to the S3 bucket. The build mustexclusively specify that static files are located in a different location than what’s relative to the index.html file. Your S3 bucket needs to be public with the right bucket policy and CORS set so that the end-user can only retrieve files and not upload nasty objects. Those who are confused about how the S3 static website and S3 public bucket differ may refer to here. Below are the react build steps, bucket policy, and CORS.

    PUBLIC_URL=https://{your-static-bucket-name}.s3.{aws_region}.amazonaws.com/ npm run build
    //Bucket Policy
    {
       "Version": "2012-10-17",
       "Id": "http referer from your domain only",
       "Statement": [
           {
               "Sid": "Allow get requests originating from",
               "Effect": "Allow",
               "Principal": "*",
               "Action": "s3:GetObject",
               "Resource": "arn:aws:s3:::{your-static-bucket-name}/static/*",
               "Condition": {
                   "StringLike": {
                       "aws:Referer": [
                           "https://your-app-domain-name"
                       ]
                   }
               }
           }
       ]
    }
    //CORS
    [
       {
           "AllowedHeaders": [
               "*"
           ],
           "AllowedMethods": [
               "GET"
           ],
           "AllowedOrigins": [
               "https://your-app-domain-name"
           ],
           "ExposeHeaders": []
       }
    ]

    Once a build is complete, upload index.html to a lambda that serves your UI. Run the below shell commands to compress static contents and host them on our static S3 bucket.

    #assuming you are in your react app directory
    mkdir /tmp/s3uploads
    cp -ar build/static /tmp/s3uploads/
    cd /tmp/s3uploads
    #add gzip encoding to all the files
    gzip -9 `find ./ -type f`
    #remove .gz extension from compressed files
    for i in `find ./ -type f`
    do
       mv $i ${i%.*}
    done
    #sync your files to s3 static bucket and mention that these files are compressed with gzip encoding
    #so that browser will not treat them as regular files
    aws s3 --region $AWSREGION sync . s3://${S3_STATIC_BUCKET}/static/ --content-encoding gzip --delete --sse
    cd -
    rm -rf /tmp/s3uploads

    Our backend uses nodejs express framework. Since this is a serverless application, we need to wrap express with a serverless-http framework to work with lambda. Sample source code is given below, along with serverless framework resource definition. Notice that, except for the UI home endpoint ( “/” ), the rest of the API endpoints are authenticated with Cognito on the API gateway itself.

    const serverless = require("serverless-http");
    const express = require("express");
    const app = express();
    .
    .
    .
    .
    .
    .
    app.get("/",(req,res)=> {
     res.sendFile(path.join(__dirname + "/index.html"));
    });
    module.exports.uihome = serverless(app);

    provider:
     name: aws
     runtime: nodejs12.x
     lambdaHashingVersion: 20201221
     httpApi:
       authorizers:
         cognitoJWTAuth:
           identitySource: $request.header.Authorization
           issuerUrl: https://cognito-idp.{AWS_REGION}.amazonaws.com/{COGNITO_USER_POOL_ID}
           audience:
             - COGNITO_APP_CLIENT_ID
    .
    .
    .
    .
    .
    .
    .
    functions:
     react-serve-ui:
       handler: handler.uihome
       memorySize: 256
       timeout: 29
       layers:
         - {Ref: CommonLibsLambdaLayer}
       events:
         - httpApi:
             path: /prep/photoupload
             method: post
             authorizer:
               name: cognitoJWTAuth
         - httpApi:
             path: /list/photos
             method: get
             authorizer:
               name: cognitoJWTAuth
         - httpApi:
             path: /
             method: get

    Final Steps :

    Lastly, we will setup up a custom domain so that we don’t need to use the gibberish domain name generated by the API gateway and certificate for our custom domain. You don’t need to use route53 for this part. If you have an existing domain, you can create a subdomain and point it to the API gateway. First things first: head to the AWS ACM console and generate a certificate for the domain name. Once the request is generated, you need to validate your domain by creating a TXT record as per the ACM console. The ACM is a free service. Domain verification may take few minutes to several hours. Once you have the certificate ready, head back to the API gateway console. Navigate to “custom domain names” and click create.

    1. Enter your application domain name
    2. Check TLS 1.2 as TLS version
    3. Select Endpoint type as Regional
    4. Select ACM certificate from dropdown list
    5. Create domain name

    Select the newly created custom domain. Note the API gateway domain name from Domain Details -> Configuration tab. You will need this to map a CNAME/ALIAS record with your DNS provider. Click on the API mappings tab. Click configure API mappings. From the dropdown, select your API gateway, select stage as default, and click save. You are done here.

    Future Scope and Improvements :

    To improve application latency, we can use CloudFront as CDN. This way, our entry point could be S3, and we no longer need to use API gateway regional endpoint. We can also add AWS WAF as an added security in front of our API gateway to inspect incoming requests and payloads. We can also use Dynamodb secondary indexes so that we can efficiently search metadata in the table. Adding a lifecycle rule on raw photos which have not been accessed for more than a year can be transited to the S3 Glacier storage class. You can further add glacier deep storage transition to save more on storage costs.

  • Autoscaling in Kubernetes using HPA and VPA

    Autoscaling, a key feature of Kubernetes, lets you improve the resource utilization of your cluster by automatically adjusting the application’s resources or replicas depending on the load at that time.

    This blog talks about Pod Autoscaling in Kubernetes and how to set up and configure autoscalers to optimize the resource utilization of your application.

    Horizontal Pod Autoscaling

    What is the Horizontal Pod Autoscaler?

    The Horizontal Pod Autoscaler (HPA) scales the number of pods of a replica-set/ deployment/ statefulset based on per-pod metrics received from resource metrics API (metrics.k8s.io) provided by metrics-server, the custom metrics API (custom.metrics.k8s.io), or the external metrics API (external.metrics.k8s.io).

    Fig:- Horizontal Pod Autoscaling

    ‍Prerequisite

    Verify that the metrics-server is already deployed and running using the command below, or deploy it using instructions here.

    kubectl get deployment metrics-server -n kube-system

    HPA using Multiple Resource Metrics‍

    HPA fetches per-pod resource metrics (like CPU, memory) from the resource metrics API and calculates the current metric value based on the mean values of all targeted pods. It compares the current metric value with the target metric value specified in the HPA spec and produces a ratio used to scale the number of desired replicas.

    A. Setup: Create a Deployment and HPA resource

    In this blog post, I have used the config below to create a deployment of 3 replicas, with some memory load defined by “–vm-bytes”, “850M”.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
     name: autoscale-tester
    spec:
     replicas: 3
     selector:
       matchLabels:
         app: autoscale-tester
     template:
       metadata:
         labels:
           app: autoscale-tester
       spec:
         containers:
         - args: [ "--vm", "1", "--vm-bytes", "850M", "--vm-hang", "1"]
           command:
           - stress
           image: polinux/stress
           name: autoscale-tester
           resources:
             limits:
               cpu: "1"
               memory: 1000Mi
             requests:
               cpu: "1"
               memory: 1000Mi

    NOTE: It’s recommended not to use HPA and VPA on the same pods or deployments.

    kubectl top po
    NAME                            	CPU(cores)   MEMORY(bytes)   
    autoscale-tester-878b8c6c8-42gmk   326m     	853Mi      	 
    autoscale-tester-878b8c6c8-gp45f   410m     	852Mi      	 
    autoscale-tester-878b8c6c8-tz4mg   388m     	852Mi 

    Lets create an HPA resource for this deployment with multiple metric blocks defined. The HPA will consider each metric one-by-one and calculate the desired replica counts based on each of the metrics, and then select the one with the highest replica count.

    apiVersion: autoscaling/v2beta2
    kind: HorizontalPodAutoscaler
    metadata:
     name: autoscale-tester
    spec:
     scaleTargetRef:
       apiVersion: apps/v1
       kind: Deployment
       name: autoscale-tester
     minReplicas: 1
     maxReplicas: 10
     metrics:
     - type: Resource
       resource:
         name: cpu
         target:
           type: Utilization
           averageUtilization: 50
     - type: Resource
       resource:
         name: memory
         target:
           type: AverageValue
           averageValue: 500Mi

    • We have defined the minimum number  of replicas HPA can scale down to as 1 and the maximum number that it can scale up to as 10.
    • Target Average Utilization and Target Average Values implies that the HPA should scale the replicas up/down to keep the Current Metric Value equal or closest to Target Metric Value.

    B. Understanding the HPA Algorithm

    kubectl describe hpa autoscale-tester
    Name:       autoscale-tester
    Namespace:  autoscale-tester
    ...
    Metrics:                                           	( current / target )
      resource memory on pods:                         	894188202666m / 500Mi
      resource cpu on pods  (as a percentage of request):  36% (361m) / 50%
    Min replicas:                                      	1
    Max replicas:                                      	10
    Deployment pods:                                   	3 current / 6 desired
    Conditions:
      Type        	Status  Reason          	Message
      ----        	------  ------          	-------
      AbleToScale 	True	SucceededRescale	the HPA controller was able to update the target scale to 6
      ScalingActive   True	ValidMetricFound	the HPA was able to successfully calculate a replica count from memory resource
      ScalingLimited  False   DesiredWithinRange  the desired count is within the acceptable range
    Events:
      Type	Reason         	Age   From                   	Message
      ----	------         	----  ----                   	-------
      Normal  SuccessfulRescale  7s	horizontal-pod-autoscaler  New size: 6; reason: memory resource above target

    • HPA calculates pod utilization as total usage of all containers in the pod divided by total request. It looks at all containers individually and returns if container doesn’t have request.
    • The calculated  Current Metric Value for memory, i,e., 894188202666m, is higher than the Target Average Value of 500Mi, so the replicas need to be scaled up.
    • The calculated  Current Metric Value for CPU i.e., 36%, is lower than the Target Average Utilization of 50, so  hence the replicas need to be scaled down.
    • Replicas are calculated based on both metrics and the highest replica count selected. So, the replicas are scaled up to 6 in this case.

    HPA using Custom metrics

    We will use the prometheus-adapter resource to expose custom application metrics to custom.metrics.k8s.io/v1beta1, which are retrieved by HPA. By defining our own metrics through the adapter’s configuration, we can let HPA perform scaling based on our custom metrics.

    A. Setup: Install Prometheus Adapter

    Create prometheus-adapter.yaml with the content below:

    prometheus:
     url: http://prometheus-server
     port: 0
    image:
     tag: latest
    rules:
     custom:
       - seriesQuery: 'container_network_receive_packets_total{namespace!="",pod!=""}'
         resources:
           overrides:
             namespace: {resource: "namespace"}
             pod: {resource: "pod"}
      	name:
          	matches: "container_network_receive_packets_total"
          	as: "packets_in"
      	metricsQuery: <<.Series>>{<<.LabelMatchers>>}

    helm install stable/prometheus -n prometheus --namespace prometheus
    helm install stable/prometheus-adapter -n prometheus-adapter --namespace prometheus -f prometheus-adapter.yaml

    Once the charts are deployed, verify the metrics are exposed at v1beta1.custom.metrics.k8s.io:

    kubectl get apiservice
    NAME                               	SERVICE                     	AVAILABLE   AGE
    v1beta1.custom.metrics.k8s.io      	prometheus/prometheus-adapter   True    	19m 
    
    
    kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1/namespaces/autoscale-hpa/pods/*/packets_in | jq
    {
      "kind": "MetricValueList",
      "apiVersion": "custom.metrics.k8s.io/v1beta1",
      "metadata": {
    	"selfLink": "/apis/custom.metrics.k8s.io/v1beta1/namespaces/autoscale-hpa/pods/%2A/packets_in"
      },
     "items": [
    	{
      	"describedObject": {
        	"kind": "Pod",
        	"namespace": "autoscale-hpa",
        	"name": "autoscale-tester-878b8c6c8-42gmk",
        	"apiVersion": "/v1"
      	},
      	"metricName": "packets_in",
      	"timestamp": "2020-07-31T05:59:33Z",
      	"value": "33",
      	"selector": null
    	},
    	{
      	"describedObject": {
        	"kind": "Pod",
        	"namespace": "autoscale-hpa",
        	"name": "autoscale-tester-878b8c6c8-hfts8",
        	"apiVersion": "/v1"
      	},
      	"metricName": "packets_in",
      	"timestamp": "2020-07-31T05:59:33Z",
      	"value": "11",
      	"selector": null
    	},
    	{
      	"describedObject": {
        	"kind": "Pod",
        	"namespace": "autoscale-hpa",
        	"name": "autoscale-tester-878b8c6c8-rb9v2",
        	"apiVersion": "/v1"
      	},
      	"metricName": "packets_in",
      	"timestamp": "2020-07-31T05:59:33Z",
      	"value": "10",
      	"selector": null
    	}
      ]
    }

    You can see the metrics value of all the replicas in the output.

    B. Understanding Prometheus Adapter Configuration

    The adapter considers metrics defined with the parameters below:

    1. seriesQuery tells the Prometheus Metric name to the adapter

    2. resources tells which Kubernetes resources each metric is associated with or which labels does the metric include, e.g., namespace, pod etc.

    3. metricsQuery is the actual Prometheus query that needs to be performed to calculate the actual values.

    4. name with which the metric should be exposed to the custom metrics API

    For instance, if we want to calculate the rate of container_network_receive_packets_total, we will need to write this query in Prometheus UI:

    sum(rate(container_network_receive_packets_total{namespace=”autoscale-tester”,pod=~”autoscale-tester.*”}[10m])) by (pod)

    This query is represented as below in the adapter configuration:

    metricsQuery: ‘sum(rate(<<.series>>{<<.labelmatchers>>}10m])) by (<<.groupby>>)'</.groupby></.labelmatchers></.series>

    C. Create an HPA resource

    Now, let’s create an HPA resource with the pod metric packets_in using the config below, and then describe the HPA resource.

    apiVersion: autoscaling/v2beta2
    kind: HorizontalPodAutoscaler
    metadata:
     name: autoscale-tester
    spec:
     scaleTargetRef:
       apiVersion: apps/v1
       kind: Deployment
       name: autoscale-tester
     minReplicas: 1
     maxReplicas: 10
     metrics:
     - type: Pods
       pods:
         metric:
           name: packets_in
         target:
           type: AverageValue
           averageValue: 50

    kubectl describe hpa autoscale-tester
    Name:                	autoscale-tester
    Namespace:           	autoscale-tester
    ...
    Metrics:             	( current / target )
      "packets_in" on pods:  18666m / 50
    Min replicas:        	1
    Max replicas:        	10
    Deployment pods:     	3 current / 3 desired
    Conditions:
      Type        	Status  Reason          	Message
      ----        	------  ------          	-------
      AbleToScale 	True	SucceededRescale	the HPA controller was able to update the target scale to 2
      ScalingActive   True	ValidMetricFound	the HPA was able to successfully calculate a replica count from pods metric packets_in
      ScalingLimited  False   DesiredWithinRange  the desired count is within the acceptable range
    Events:
      Type	Reason         	Age   From                   	Message
      ----	------         	----  ----                   	-------
      Normal  SuccessfulRescale  2s	horizontal-pod-autoscaler  New size: 2; reason: All metrics below target
      Normal  SuccessfulRescale  2m51s  horizontal-pod-autoscaler  New size: 1; reason: All metrics below target 
    kubectl describe hpa autoscale-tester
    Name:                	autoscale-tester
    Namespace:           	autoscale-tester
    ...
    Metrics:             	( current / target )
      "packets_in" on pods:  18666m / 50
    Min replicas:        	1
    Max replicas:        	10
    Deployment pods:     	3 current / 3 desired
    Conditions:
      Type        	Status  Reason          	Message
      ----        	------  ------          	-------
      AbleToScale 	True	SucceededRescale	the HPA controller was able to update the target scale to 2
      ScalingActive   True	ValidMetricFound	the HPA was able to successfully calculate a replica count from pods metric packets_in
      ScalingLimited  False   DesiredWithinRange  the desired count is within the acceptable range
    Events:
      Type	Reason         	Age   From                   	Message
      ----	------         	----  ----                   	-------
      Normal  SuccessfulRescale  2s	horizontal-pod-autoscaler  New size: 2; reason: All metrics below target
      Normal  SuccessfulRescale  2m51s  horizontal-pod-autoscaler  New size: 1; reason: All metrics below target 

    Here, the current calculated metric value is 18666m. The m represents milli-units. So, for example, 18666m means 18.666 which is what we expect ((33 + 11 + 10 )/3 = 18.666). Since it’s less than the target average value (i.e., 50), the HPA scales down the replicas to make the Current Metric Value : Target Metric Value ratio closest to 1. Hence, replicas are scaled down to 2 and later to 1.

    Fig:- container_network_receive_packets_total

     

    Fig:- Ratio to Target value

    ‍Vertical Pod Autoscaling

    What is Vertical Pod Autoscaler?

    Vertical Pod autoscaling (VPA) ensures that a container’s resources are not under- or over-utilized. It recommends optimized CPU and memory requests/limits values, and can also automatically update them for you so that the cluster resources are efficiently used.

    Fig:- Vertical Pod Autoscaling

    Architecture

    VPA consists of 3 components:

    • VPA admission controller
      Once you deploy and enable the Vertical Pod Autoscaler in your cluster, every pod submitted to the cluster goes through this webhook, which checks whether a VPA object is referencing it.
    • VPA recommender
      The recommender pulls the current and past resource consumption (CPU and memory) data for each container from metrics-server running in the cluster and provides optimal resource recommendations based on it, so that a container uses only what it needs.
    • VPA updater
      The updater checks at regular intervals if a pod is running within the recommended range. Otherwise, it accepts it for update, and the pod is evicted by the VPA updater to apply resource recommendation.

    Installation

    If you are on Google Cloud Platform, you can simply enable vertical-pod-autoscaling:

    gcloud container clusters update <cluster-name> --enable-vertical-pod-autoscaling

    To install it manually follow below steps:

    • Verify that the metrics-server deployment is running, or deploy it using instructions here.
    kubectl get deployment metrics-server -n kube-system

    • Also, verify the API below is enabled:
    kubectl api-versions | grep admissionregistration
    admissionregistration.k8s.io/v1beta1

    • Clone the kubernetes/autoscaler GitHub repository, and then deploy the Vertical Pod Autoscaler with the following command.
    git clone https://github.com/kubernetes/autoscaler.git
    ./autoscaler/vertical-pod-autoscaler/hack/vpa-up.sh

    Verify that the Vertical Pod Autoscaler pods are up and running:

    kubectl get po -n kube-system
    NAME                                        READY   STATUS    RESTARTS   AGE
    vpa-admission-controller-68c748777d-ppspd   1/1     Running   0          7s
    vpa-recommender-6fc8c67d85-gljpl            1/1     Running   0          8s
    vpa-updater-786b96955c-bgp9d                1/1     Running   0          8s
    
    kubectl get crd
    verticalpodautoscalers.autoscaling.k8s.io 

    VPA using Resource Metrics

    A. Setup: Create a Deployment and VPA resource

    Use the same deployment config to create a new deployment with “–vm-bytes”, “850M”. Then create a VPA resource in Recommendation Mode with updateMode : Off

    apiVersion: autoscaling.k8s.io/v1beta2
    kind: VerticalPodAutoscaler
    metadata:
     name: autoscale-tester-recommender
    spec:
     targetRef:
       apiVersion: "apps/v1"
       kind:       Deployment
       name:       autoscale-tester
     updatePolicy:
       updateMode: "Off"
     resourcePolicy:
       containerPolicies:
       - containerName: autoscale-tester
         minAllowed:
           cpu: "500m"
           memory: "500Mi"
         maxAllowed:
           cpu: "4"
           memory: "8Gi"

    • minAllowed is an optional parameter that specifies the minimum CPU request and memory request allowed for the container. 
    • maxAllowed is an optional parameter that specifies the maximum CPU request and memory request allowed for the container.

    B. Check the Pod’s Resource Utilization

    Check the resource utilization of the pods. Below, you can see only ~50 Mi memory is being used out of 1000Mi and only ~30m CPU out of 1000m. This clearly indicates that the pod resources are underutilized.

    Kubectl top po
    NAME                            	CPU(cores)   MEMORY(bytes)   
    autoscale-tester-5d6b48d64f-8zgb9   39m      	51Mi       	 
    autoscale-tester-5d6b48d64f-npts4   32m      	50Mi       	 
    autoscale-tester-5d6b48d64f-vctx5   35m      	50Mi 

    If you describe the VPA resource, you can see the Recommendations provided. (It may take some time to show them.)

    kubectl describe vpa autoscale-tester-recommender
    Name:     	autoscale-tester-recommender
    Namespace:	autoscale-tester
    ...
      Recommendation:
    	Container Recommendations:
      	Container Name:  autoscale-tester
      	Lower Bound:
        	Cpu: 	500m
        	Memory:  500Mi
      	Target:
        	Cpu: 	500m
        	Memory:  500Mi
      	Uncapped Target:
        	Cpu: 	93m
        	Memory:  262144k
      	Upper Bound:
        	Cpu: 	4
        	Memory:  4Gi

    C. Understand the VPA recommendations

    Target: The recommended CPU request and memory request for the container that will be applied to the pod by VPA.

    Uncapped Target: The recommended CPU request and memory request for the container if you didn’t configure upper/lower limits in the VPA definition. These values will not be applied to the pod. They’re used only as a status indication.

    Lower Bound: The minimum recommended CPU request and memory request for the container. There is a –pod-recommendation-min-memory-mb flag that determines the minimum amount of memory the recommender will set—it defaults to 250MiB.

    Upper Bound: The maximum recommended CPU request and memory request for the container.  It helps the VPA updater avoid eviction of pods that are close to the recommended target values. Eventually, the Upper Bound is expected to reach close to target recommendation.

     Recommendation:
    	Container Recommendations:
      	Container Name:  autoscale-tester
      	Lower Bound:
        	Cpu: 	500m
        	Memory:  500Mi
      	Target:
        	Cpu: 	500m
        	Memory:  500Mi
      	Uncapped Target:
        	Cpu: 	93m
        	Memory:  262144k
      	Upper Bound:
        	Cpu: 	500m
        	Memory:  1274858485 

    D. VPA processing with Update Mode Off/Auto

    Now, if you check the logs of vpa-updater, you can see it’s not processing VPA objects as the Update Mode is set as Off.

    kubectl logs -f vpa-updater-675d47464b-k7xbx
    1 updater.go:135] skipping VPA object autoscale-tester-recommender because its mode is not "Recreate" or "Auto"
    1 updater.go:151] no VPA objects to process

    VPA allows various Update Modes, detailed here.

    Let’s change the VPA updateMode to “Auto” to see the processing.

    As soon as you do that, you can see vpa-updater has started processing objects, and it’s terminating all 3 pods.

    kubectl logs -f vpa-updater-675d47464b-k7xbx
    1 update_priority_calculator.go:147] pod accepted for update autoscale-tester/autoscale-tester-5d6b48d64f-8zgb9 with priority 1
    1 update_priority_calculator.go:147] pod accepted for update autoscale-tester/autoscale-tester-5d6b48d64f-npts4 with priority 1
    1 update_priority_calculator.go:147] pod accepted for update autoscale-tester/autoscale-tester-5d6b48d64f-vctx5 with priority 1
    1 updater.go:193] evicting pod autoscale-tester-5d6b48d64f-8zgb9
    1 event.go:281] Event(v1.ObjectReference{Kind:"Pod", Namespace:"autoscale-tester", Name:"autoscale-tester-5d6b48d64f-8zgb9", UID:"ed8c54c7-a87a-4c39-a000-0e74245f18c6", APIVersion:"v1", ResourceVersion:"378376", FieldPath:""}): 
    type: 'Normal' reason: 'EvictedByVPA' Pod was evicted by VPA Updater to apply resource recommendation.

    You can also check the logs of vpa-admission-controller:

    kubectl logs -f vpa-admission-controller-bbf4f4cc7-cb6pb
    Sending patches: [{add /metadata/annotations map[]} {add /spec/containers/0/resources/requests/cpu 500m} {add /spec/containers/0/resources/requests/memory 500Mi} {add /spec/containers/0/resources/limits/cpu 500m} {add /spec/containers/0/resources/limits/memory 500Mi} {add /metadata/annotations/vpaUpdates Pod resources updated by autoscale-tester-recommender: container 0: cpu request, memory request, cpu limit, memory limit} {add /metadata/annotations/vpaObservedContainers autoscale-tester}]

    NOTE: Ensure that you have more than 1 running replicas. Otherwise, the pods won’t be restarted, and vpa-updater will give you this warning:

    1 pods_eviction_restriction.go:209] too few replicas for ReplicaSet autoscale-tester/autoscale-tester1-7698974f6. Found 1 live pods

    Now, describe the new pods created and check that the resources match the Target recommendations:

    kubectl get po
    NAME                            	READY   STATUS    	RESTARTS   AGE
    autoscale-tester-5d6b48d64f-5dlb7   1/1 	Running   	0      	77s
    autoscale-tester-5d6b48d64f-9wq4w   1/1 	Running   	0      	37s
    autoscale-tester-5d6b48d64f-qrlxn   1/1 	Running   	0      	17s
    
    
    kubectl describe po autoscale-tester-5d6b48d64f-5dlb7
    Name:     	autoscale-tester-5d6b48d64f-5dlb7
    Namespace:	autoscale-tester
    ...
    	Limits:
      	cpu: 	500m
      	memory:  500Mi
    	Requests:
      	cpu:    	500m
      	memory: 	500Mi
    	Environment:  <none>

    The Target Recommendation can not go below the minAllowed defined in the VPA spec.

    Fig:- Prometheus: Memory Usage Ratio

    E. Stress Loading Pods

    Let’s recreate the deployment with memory request and limit set to 2000Mi and “–vm-bytes”, “500M”.

    Gradually stress load one of these pods to increase its memory utilization.
    You can login to the pod and run stress –vm 1 –vm-bytes 1400M –timeout 120000s.

    
    kubectl top po
    NAME                            	CPU(cores)   MEMORY(bytes)   
    autoscale-tester-5d6b48d64f-5dlb7   1000m     	1836Mi       	 
    autoscale-tester-5d6b48d64f-9wq4w   252m      	501Mi       	 
    autoscale-tester-5d6b48d64f-qrlxn   252m      	501Mi 	

    Fig:- Prometheus memory utilized by each Replica

    You will notice that the VPA recommendation is also calculated accordingly and applied to all replicas.

    kubectl describe vpa autoscale-tester-recommender
    Name:     	autoscale-tester-recommender
    Namespace:	autoscale-tester
    ...
      Recommendation:
    	Container Recommendations:
      	Container Name:  autoscale-tester
      	Lower Bound:
        	Cpu: 	500m
        	Memory:  500Mi
      	Target:
        	Cpu: 	500m
        	Memory:  628694953
      	Uncapped Target:
        	Cpu: 	49m
        	Memory:  628694953
      	Upper Bound:
        	Cpu: 	500m
        	Memory:  1553712527

    Limits v/s Request
    VPA always works with the requests defined for a container and not the limits. So, the VPA recommendations are also applied to the container requests, and it maintains a limit to request ratio specified for all containers.

    For example, if the initial container configuration defines a 100m Memory Request and 300m Memory Limit, then when the VPA target recommendation is 150m Memory, the container Memory Request will be updated to 150m and Memory Limit to 450m.

    Selective Container Scaling

    If you have a pod with multiple containers and you want to opt-out some of them, you can use the “Off” mode to turn off recommendations for a container.

    You can also set containerName: “*” to include all containers.

    spec:
     targetRef:
       apiVersion: "apps/v1"
       kind:       Deployment
       name:       autoscale-tester
     updatePolicy:
       updateMode: "Auto"
     resourcePolicy:
       containerPolicies:
       - containerName: autoscale-tester
         minAllowed:
           cpu: "500m"
           memory: "500Mi"
         maxAllowed:
           cpu: "4"
           memory: "4Gi"
       - containerName: opt-out-container
         mode: "Off"

    Conclusion

    Both the Horizontal Pod Autoscaler and the Vertical Pod Autoscaler serve different purposes and one can be more useful than the other depending on your application’s requirement.

    The HPA can be useful when, for example, your application is serving a large number of lightweight (low resource-consuming) requests. In that case, scaling number of replicas can distribute the workload on each of the pod. The VPA, on the other hand, can be useful when your application serves heavyweight requests, which requires higher resources.

    Related Articles:

    1. A Practical Guide to Deploying Multi-tier Applications on Google Container Engine (GKE)

    2. Know Everything About Spinnaker & How to Deploy Using Kubernetes Engine

  • How to Implement Server Sent Events Using Python Flask and React

    A typical Request Response cycle works such that client sends request to server and server responds to that request. But there are few use cases where we might need to send data from server without request or client is expecting a data that can arrive at anonymous time. There are few mechanisms available to solve this problem.

    Server Sent Events

    Broadly we can classify these as client pull and server push mechanisms. Websockets is a bi directional mechanism where data is transmitted via full duplex TCP protocol. Client Pull can be done using various mechanisms like –

    1. Manual refresh – where client is refreshed manually
    2. Long polling where a client sends request to server and waits until response is received, as soon as it gets response, a new request is sent.
    3. Short Polling is when a client continuously sends request to server in a definite short intervals.

    Server sent events are a type of Server Push mechanism, where client subscribes to a stream of updates generated by a server and, whenever a new event occurs, a notification is sent to the client.

    Why ServerSide events are better than polling:

    • Scaling and orchestration of backend in real time needs to be managed as users grow.
    • When mobile devices rapidly switch between WiFi and cellular networks or lose connections, and the IP address changes, long polling needs to re-establish connections.
    • With long polling, we need to manage the message queue and catch up missed message.
    • Long polling needs to provide load balancing or fail-over support across multiple servers.

    SSE vs Websockets

    SSEs cannot provide bidirectional client-server communication as opposed to WebSockets. Use cases that require such communication are real-time multiplayer games and messaging and chat apps. When there’s no need for sending data from a client, SSEs might be a better option than WebSockets. Examples of such use cases are status updates, news feeds and other automated data push mechanisms. And backend implementation could be easy with SSE than with Websockets. Also number of open connections is limited for browser for SSE.

    Also, learn about WS vs SSE here.

    Implementation

    The server side code for this can be implemented in any of the high level language. Here is a sample code for Python Flask SSE. Flask SSE requires a broker such as Redis to store the message. Here we are also using Flask APScheduler, to schedule background processes with flask .

    Here we need to install and import ‘flask_sse’ and ‘apscheduler.’

    from flask import Flask, render_template
    from flask_sse import sse
    from apscheduler.schedulers.background import BackgroundScheduler

    Now we need to initialize flask app and provide config for Redis and a route or an URL where the client would be listening to this event.

    app = Flask(__name__)
    app.config["REDIS_URL"] = "redis://localhost"
    app.register_blueprint(sse, url_prefix='/stream')

    To publish data to a stream we need to call publish method from SSE and provide a type of stream.

    sse.publish({"message": datetime.datetime.now()}, type='publish')

    In client, we need to add an event listener which would listen to our stream and read messages.

    var source = new EventSource("{{ url_for('sse.stream') }}");
        source.addEventListener('publish', function(event) {
            var data = JSON.parse(event.data);
            console.log("The server says " + data.message);
        }, false);
        source.addEventListener('error', function(event) {
            console.log("Error"+ event)
            alert("Failed to connect to event stream. Is Redis running?");
        }, false);

    Check out a sample Flask-React-Redis based application demo for server side events.

    Here are some screenshots of client –

    Fig: First Event

     Fig: Second Event

    Server logs:

    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 31, 0, 24564))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 31, 14, 30164))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 31, 28, 37840))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 31, 42, 58162))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 31, 56, 46456))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 32, 10, 56276))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 32, 24, 58445))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 32, 38, 57183))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 32, 52, 65886))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 33, 6, 49818))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 33, 20, 22731))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 33, 34, 59084))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 33, 48, 70346))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 34, 2, 58889))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 34, 16, 26020))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 34, 30, 44040))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 34, 44, 61620))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 34, 58, 38699))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 35, 12, 26067))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 35, 26, 71504))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 35, 40, 31429))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 35, 54, 74451))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 36, 8, 63001))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 36, 22, 47671))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 36, 36, 55458))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 36, 50, 68975))
    api_1    | ('Event Scheduled at ', datetime.datetime(2019, 5, 1, 7, 37, 4, 62491))
    api_1    | ('Event SchedINFO:apscheduler.executors.default:Job "server_side_event (trigger: interval[0:00:14], next run at: 2019-05-01 07:37:31 UTC)" executed successfully
    api_1    | INFO:apscheduler.executors.default:Running job "server_side_event (trigger: interval[0:00:16], next run at: 2019-05-01 07:37:38 UTC)" (scheduled at 2019-05-01 07:37:22.362874+00:00)
    api_1    | INFO:apscheduler.executors.default:Job "server_side_event (trigger: interval[0:00:16], next run at: 2019-05-01 07:37:38 UTC)" executed successfully
    api_1    | INFO:apscheduler.executors.default:Running job "server_side_event (trigger: interval[0:00:14], next run at: 2019-05-01 07:37:31 UTC)" (scheduled at 2019-05-01 07:37:31.993944+00:00)
    api_1    | INFO:apscheduler.executors.default:Job "server_side_event (trigger: interval[0:00:14], next run at: 2019-05-01 07:37:45 UTC)" executed successfully
    api_1    | INFO:apscheduler.executors.default:Running job "server_side_event (trigger: interval[0:00:16], next run at: 2019-05-01 07:37:54 UTC)" (scheduled at 2019-05-01 07:37:38.362874+00:00)
    api_1    | INFO:apscheduler.executors.default:Job "server_side_event (trigger: interval[0:00:16], next run at: 2019-05-01 07:37:54 UTC)" executed successfully

    Use Cases of Server Sent Events

    Let’s see the use case with an example – Consider we have a real time graph showing on our web app, one of the possible options is polling where continuously client will poll the server to get new data. Other option would be to use server sent events, which are asynchronous, here the server will send data when updates happen.

    Other applications could be

    • Real time stock price analysis system
    • Real time social media feeds
    • Resource monitoring for health, uptime

    Conclusion

    In this blog, we have covered how we can implement server sent events using Python Flask and React and also how we can use background schedulers with that. This can be used to implement a data delivery from the server to the client using server push.

  • OPA On Kubernetes: An Introduction For Beginners

    Introduction:

    More often than not organizations need to apply various kinds of policies on the environments where they run their applications. These policies might be required to meet compliance requirements, achieve a higher degree of security, achieve standardization across multiple environments, etc. This calls for an automated/declarative way to define and enforce these policies. Policy engines like OPA help us achieve the same. 

    Motivation behind Open Policy Agent (OPA)

    When we run our application, it generally comprises multiple subsystems. Even in the simplest of cases, we will be having an API gateway/load balancer, 1-2 applications and a database. Generally, all these subsystems will have different mechanisms for authorizing the requests, for example, the application might be using JWT tokens to authorize the request, but your database is using grants to authorize the request, it is also possible that your application is accessing some third-party APIs or cloud services which will again have a different way of authorizing the request. Add to this your CI/CD servers, your log server, etc and you can see how many different ways of authorization can exist even in a small system. 

    The existence of so many authorization models in our system makes life difficult when we need to meet compliance or information security requirements or even some self-imposed organizational policies. For example, if we need to adhere to some new compliance requirements then we need to understand and implement the same for all the components which do authorization in our system.

    “The main motivation behind OPA is to achieve unified policy enforcements across the stack

    What are Open Policy Agent (OPA) and OPA Gatekeeper

    The OPA is an open-source, general-purpose policy engine that can be used to enforce policies on various types of software systems like microservices, CI/CD pipelines, gateways, Kubernetes, etc. OPA was developed by Styra and is currently a part of CNCF.

    OPA provides us with REST APIs which our system can call to check if the policies are being met for a request payload or not. It also provides us with a high-level declarative language, Rego which allows us to specify the policies we want to enforce as code. This provides us with lots of flexibility while defining our policies.

    The above image shows the architecture of OPA. It exposes APIs which any service that needs to make an authorization or policy decision, can call (policy query) and then OPA can make a decision based on the Rego code for the policy and return a decision to the service that further processes the request accordingly. The enforcement is done by the actual service itself, OPA is responsible only for making the decision. This is how OPA becomes a general-purpose policy engine and supports a large number of services.   

    The Gatekeeper project is a Kubernetes specific implementation of the OPA. Gatekeeper allows us to use OPA in a Kubernetes native way to enforce the desired policies. 

    How Gatekeeper enforces policies

    On the Kubernetes cluster, the Gatekeeper is installed as a ValidatingAdmissionWebhook. The Admission Controllers can intercept requests after they have been authenticated and authorized by the K8s API server, but before they are persisted in the database. If any of the admission controllers rejects the request then the overall request is rejected. The limitation of admission controllers is that they need to be compiled into the kube-apiserver and can be enabled only when the apiserver starts up. 

    To overcome this rigidity of the admission controller, admission webhooks were introduced. Once we enable admission webhooks controllers in our cluster, they can send admission requests to external HTTP callbacks and receive admission responses. Admission webhook can be of two types MutatingAdmissionWebhook and ValidatingAdmissionWebhook. The difference between the two is that mutating webhooks can modify the objects that they receive while validating webhooks cannot. The below image roughly shows the flow of an API request once both mutating and validating admission controllers are enabled.

     

    The role of Gatekeeper is to simply check if the request meets the defined policy or not, that is why it is installed as a validating webhook.

    Demo:

    Install Gatekeeper:

    kubectl apply -f
    https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml

    Now we have Gatekeeper up and running in our cluster. The above installation also created a CRD named `constrainttemplates.templates.gatekeeper.sh’. This CRD allows us to create constraint templates for the policy we want to enforce. In the constraint template, we define the constraints logic using the Rego code and also its schema. Once the constraint template is created, we can create the constraints which are instances of the constraint templates, created for specific resources. Think of it as function and actual function calls, the constraint templates are like functions that are invoked with different values of the parameter (resource kind and other values) by constraints.

    To get a better understanding of the same, let’s go ahead and create constraints templates and constraints.

    The policy that we want to enforce is to prevent developers from creating a service of type LoadBalancer in the `dev` namespace of the cluster, where they verify the working of other code. Creating services of type LoadBalancer in the dev environment is adding unnecessary costs. 

    Below is the constraint template for the same.

    apiVersion: templates.gatekeeper.sh/v1beta1
    kind: ConstraintTemplate
    metadata:
      name: lbtypesvcnotallowed
    spec:
      crd:
        spec:
          names:
            kind: LBTypeSvcNotAllowed
            listKind: LBTypeSvcNotAllowedList
            plural: lbtypesvcnotallowed
            singular: lbtypesvcnotallowed
      targets:
        - target: admission.k8s.gatekeeper.sh
          rego: |
            package kubernetes.admission
            violation[{"msg": msg}] {
                        input.review.kind.kind = "Service"
                        input.review.operation = "CREATE"
                        input.review.object.spec.type = "LoadBalancer"
                        msg := "LoadBalancer Services are not permitted"
            }

    In the constraint template spec, we define a new object kind/type which we will use while creating the constraints, then in the target, we specify the Rego code which will verify if the request meets the policy or not. In the Rego code, we specify a violation that if the request is to create a service of type LoadBalancer then the request should be denied.

    Using the above template, we can now define constraints:

    apiVersion: constraints.gatekeeper.sh/v1beta1
    kind: LBTypeSvcNotAllowed
    metadata:
      name: deny-lb-type-svc-dev-ns
    spec:
      match:
        kinds:
          - apiGroups: [""]
            kinds: ["Service"]
        namespaces:
          - "dev"

    Here we have specified the kind of the Kubernetes object (Service) on which we want to apply the constraint and we have specified the namespace as dev because we want the constraint to be enforced only on the dev namespace.

    Let’s go ahead and create the constraint template and constraint:

    Note: After creating the constraint template, please check if its status is true or not, otherwise you will get an error while creating the constraints. Also it is advisable to verify the Rego code snippet before using them in the constraints template.

    Now let’s try to create a service of type LoadBalancer in the dev namespace:

    kind: Service
    apiVersion: v1
    metadata:
      name: opa-service
    spec:
      type: LoadBalancer
      selector:
        app: opa-app
      ports:
      - protocol: TCP
        port: 80
        targetPort: 8080

    When we tried to create a service of type LoadBalancer in the dev namespace, we got the error that it was denied by the admission webhook due to `deny-lb-type-svc-dev-ns` constraint, but when we try to create the service in the default namespace, we were able to do so.

    Here we are not passing any parameters to the Rego policy from our constraints, but we can certainly do so to make our policy more generic, for example, we can add a field named servicetype to constraint template and in the policy code, deny all the request where the servicetype value defined in the constraint matches the value of the request. With this, we will be able to deny service of types other than LoadBalancer as well in any namespace of our cluster.

    Gatekeeper also provides auditing for resources that were created before the constraint was applied. The information is available in the status of the constraint objects. This helps us in identifying which objects in our cluster are not compliant with our constraints. 

    Conclusion:

    OPA allows us to apply fine-grained policies in our Kubernetes clusters and can be instrumental in improving the overall security of Kubernetes clusters which has always been a concern for many organizations while adopting or migrating to Kubernetes. It also makes meeting the compliance and audit requirements much simpler. There is some learning curve as we need to get familiar with Rego to code our policies, but the language is very simple and there are quite a few good examples to help in getting started.

  • How to Write Jenkinsfile for Angular and .Net Based Applications

    If you landed here directly and want to know how to setup Jenkins master-slave architecture, please visit this post related to Setting-up the Jenkins Master-Slave Architecture.

    The source code that we are using here is also a continuation of the code that was written in this GitHub Packer-Terraform-Jenkins repository.

    Creating Jenkinsfile

    We will create some Jenkinsfile to execute a job from our Jenkins master.

    Here I will create two Jenkinsfile ideally, it is expected that your Jenkinsfile is present in source code repo but it can be passed directly in the job as well.

    There are 2 ways of writing Jenkinsfile – Scripted and Declarative. You can find numerous points online giving their difference. We will be creating both of them to do a build so that we can get a hang of both of them.

    Jenkinsfile for Angular App (Scripted)

    As mentioned before we will be highlighting both formats of writing the Jenkinsfile. For the Angular app, we will be writing a scripted one but can be easily written in declarative format too.

    We will be running this inside a docker container. Thus, the tests are also going to get executed in a headless manner.

    Here is the Jenkinsfile for reference.

    Here we are trying to leverage Docker volume to keep updating our source code on bare metal and use docker container for the environments.

    Dissecting Node App’s Jenkinsfile

    1. We are using CleanWs() to clear the workspace.
    2. Next is the Main build in which we define our complete build process.
    3. We are pulling the required images.
    4. Highlighting the steps that we will be executing.
    5. Checkout SCM: Checking out our code from Git
    6. We are now starting the node container inside of which we will be running npm install and npm run lint.
    7. Get test dependency: Here we are downloading chrome.json which will be used in the next step when starting the container.
    8. Here we test our app. Specific changes for running the test are mentioned below.
    9. Build: Finally we build the app.
    10. Deploy: Once CI is completed we need to start with CD. The CD itself can be a blog of itself but wanted to highlight what basic deployment would do.
    11. Here we are using Nginx container to host our application.
    12. If the container does not exist it will create a container and use the “dist” folder for deployment.
    13. If Nginx container exists, then it will ask for user input to recreate a container or not.
    14. If you select not to create, don’t worry as we are using Nginx it will do a hot reload with new changes.

    The angular application used here was created using the standard generate command given by the CLI itself. Although the build and install give no trouble in a bare metal some tweaks are required for running test in a container.

    In karma.conf.js update browsers withChromeHeadless.

    Next in protractor.conf.js update browserName with chrome and add

    chromeOptions': {
    args': ['--headless', '--disable-gpu', '--window-size=800x600']
    },

    That’s it! And We have our CI pipeline setup for Angular based application.

    Jenkinsfile for .Net App (Declarative)

    For a .Net application, we have to setup MSBuild and MSDeploy. In the blog post mentioned above, we have already setup MSBuild and we will shortly discuss how to setup MSDeploy.

    To do the Windows deployment we have two options. Either setup MSBuild in Jenkins Global Tool Configuration or use the full path of MSBuild on the slave machine.

    Passing the path is fairly simple and here we will discuss how to use global tool configuration in a Jenkinsfile.

    First, get the path of MSBuild from your server. If it is not the latest version then the path is different and is available in Current directory otherwise always in <version> directory.</version>

    As we are using MSBuild 2017. Our MSBuild path is:

    C:Program Files (x86)Microsoft Visual Studio2017BuildToolsMSBuild15.0Bin

    Place this in /configureTools/ —> MSBuild

    Now you have your configuration ready to be used in Jenkinsfile.

    Jenkinsfile to build and test the app is given below.

    As seen above the structure of Declarative syntax is almost same as that of Declarative. Depending upon which one you find easier to read you should opt the syntax.

    Dissecting Dotnet App’s Jenkinsfile

    1. In this case too we are cleaning the workspace as the first step.
    2. Checkout: This is also the same as before.
    3. Nuget Restore: We are downloading dependent required packages for both PrimeService and PrimeService.Tests
    4. Build: Building the Dotnet app using MSBuild tool which we had configured earlier before writing the Jenkinsfile.
    5. UnitTest: Here we have used dotnet test although we could’ve used MSTest as well here just wanted to highlight how easy dotnet utility makes it. We can even use dotnet build for the build as well.
    6. Deploy: Deploying on the IIS server. Creation of IIS we are covering below.

    From the above-given examples, you get a hang of what Jenkinsfile looks like and how it can be used for creating jobs. Above file highlights basic job creation but it can be extended to everything that old-style job creation could do.

    Creating IIS Server

    Unlike our Angular application where we just had to get another image and we were good to go. Here we will have to Packer to create our IIS server. We will be automating the creation process and will be using it to host applications.

    Here is a Powershell script for IIS for reference.

    # To list all Windows Features: dism /online /Get-Features
    # Get-WindowsOptionalFeature -Online 
    # LIST All IIS FEATURES: 
    # Get-WindowsOptionalFeature -Online | where FeatureName -like 'IIS-*'
    
    # NetFx dependencies
    dism /online /Enable-Feature /FeatureName:NetFx4 /All
    
    # ASP dependencies
    dism /online /enable-feature /all /featurename:IIS-ASPNET45
    
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerRole
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServer 
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-CommonHttpFeatures
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-Security 
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-RequestFiltering 
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-StaticContent
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-DefaultDocument
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-DirectoryBrowsing
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-HttpErrors 
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-ApplicationDevelopment
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebSockets 
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-ApplicationInit
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-NetFxExtensibility45
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-ISAPIExtensions
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-ISAPIFilter
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-ASP
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-ASPNET45
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-ServerSideIncludes
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-HealthAndDiagnostics
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-HttpLogging 
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-Performance
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-HttpCompressionStatic
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerManagementTools
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-ManagementConsole 
    Enable-WindowsOptionalFeature -Online -FeatureName IIS-ManagementService
    
    # Install Chocolatey
    Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
    
    # Install WebDeploy (It will deploy 3.6)
    choco install webdeploy -y

    We won’t be deploying any application on it as we have created a sample app for PrimeNumber. But in the real world, you might be deploying Web Based application and you will need IIS. We have covered here the basic idea of how to install IIS along with any dependency that might be required.

    Conclusion

    In this post, we have covered deploying Windows and Linux based applications using Jenkinsfile in both scripted and declarative format.

    Thanks for Reading! Till next time…!!

  • An Innovator’s Guide to Kubernetes Storage Using Ceph

    Kubernetes, the awesome container orchestration tool is changing the way applications are being developed and deployed. You can specify the required resources you want and have it available without worrying about the underlying infrastructure. Kubernetes is way ahead in terms of high availability, scaling, managing your application, but storage section in the k8s is still evolving. Many storage supports are getting added and are production ready.

    People are preferring clustered applications to store the data. But, what about the non-clustered applications? Where does these applications store data to make it highly available? Considering these questions, let’s go through the Ceph storage and its integration with Kubernetes.

    What is Ceph Storage?

    Ceph is open source, software-defined storage maintained by RedHat. It’s capable of block, object, and file storage. The clusters of Ceph are designed in order to run on any hardware with the help of an algorithm called CRUSH (Controlled Replication Under Scalable Hashing). This algorithm ensures that all the data is properly distributed across the cluster and data quickly without any constraints. Replication, Thin provisioning, Snapshots are the key features of the Ceph storage.

    There are good storage solutions like Gluster, Swift but we are going with Ceph for following reasons:

    1. File, Block, and Object storage in the same wrapper.
    2. Better transfer speed and lower latency
    3. Easily accessible storage that can quickly scale up or down

    We are going to use 2 types of storage in this blog to integrate with kubernetes.

    1. Ceph-RBD
    2. CephFS

    Ceph Deployment

    Deploying highly available Ceph cluster is pretty straightforward and easy. I am assuming that you are familiar with setting up the Ceph cluster. If not then refer the official document here.

    If you check the status, you should see something like:

    # ceph -s
      cluster:
        id:     ed0bfe4e-f44c-4797-9bc6-21a988b645c7
        health: HEALTH_OK
     
      services:
        mon: 3 daemons, quorum ip-10-0-1-118,ip-10-0-1-172,ip-10-0-1-227
        mgr: ip-10-0-1-118(active), standbys: ip-10-0-1-227, ip-10-0-1-172
        mds: cephfs-1/1/1 up  {0=ip-10-0-1-118=up:active}
        osd: 3 osds: 3 up, 3 in
     
      data:
        pools:   2 pools, 160 pgs
        objects: 22  objects, 19 KiB
        usage:   3.0 GiB used, 21 GiB / 24 GiB avail
        pgs:     160 active+clean
    @velotiotech

    Here notice that my Ceph monitors IPs are 10.0.1.118, 10.0.1.227 and 10.0.1.172

    K8s Integration

    After setting up the Ceph cluster, we would consume it with Kubernetes.  I am assuming that your Kubernetes cluster is up and running. We will be using Ceph-RBD and CephFS as storage in Kubernetes.

    Ceph-RBD and Kubernetes

    We need a Ceph RBD client to achieve interaction between Kubernetes cluster and CephFS. This client is not in the official kube-controller-manager container so let’s try to create the external storage plugin for Ceph.

    kind: ClusterRole
    apiVersion: rbac.authorization.k8s.io/v1
    metadata:
      name: rbd-provisioner
    rules:
      - apiGroups: [""]
        resources: ["persistentvolumes"]
        verbs: ["get", "list", "watch", "create", "delete"]
      - apiGroups: [""]
        resources: ["persistentvolumeclaims"]
        verbs: ["get", "list", "watch", "update"]
      - apiGroups: ["storage.k8s.io"]
        resources: ["storageclasses"]
        verbs: ["get", "list", "watch"]
      - apiGroups: [""]
        resources: ["events"]
        verbs: ["create", "update", "patch"]
      - apiGroups: [""]
        resources: ["services"]
        resourceNames: ["kube-dns","coredns"]
        verbs: ["list", "get"]
      - apiGroups: [""]
        resources: ["endpoints"]
        verbs: ["get", "list", "watch", "create", "update", "patch"]
    ---
    kind: ClusterRoleBinding
    apiVersion: rbac.authorization.k8s.io/v1
    metadata:
      name: rbd-provisioner
    subjects:
      - kind: ServiceAccount
        name: rbd-provisioner
        namespace: kube-system
    roleRef:
      kind: ClusterRole
      name: rbd-provisioner
      apiGroup: rbac.authorization.k8s.io
    ---
    apiVersion: rbac.authorization.k8s.io/v1beta1
    kind: Role
    metadata:
      name: rbd-provisioner
    rules:
    - apiGroups: [""]
      resources: ["secrets"]
      verbs: ["get"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: rbd-provisioner
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: rbd-provisioner
    subjects:
    - kind: ServiceAccount
      name: rbd-provisioner
      namespace: kube-system
    ---
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: rbd-provisioner
    ---
    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: rbd-provisioner
    spec:
      replicas: 1
      strategy:
        type: Recreate
      template:
        metadata:
          labels:
            app: rbd-provisioner
        spec:
          containers:
          - name: rbd-provisioner
            image: "quay.io/external_storage/rbd-provisioner:latest"
            env:
            - name: PROVISIONER_NAME
              value: ceph.com/rbd
          serviceAccount: rbd-provisioner

    # kubectl create -n kube-system -f  Ceph-RBD-Provisioner.yaml

    • You will get output like this:
    clusterrole.rbac.authorization.k8s.io/rbd-provisioner created
    clusterrolebinding.rbac.authorization.k8s.io/rbd-provisioner created
    role.rbac.authorization.k8s.io/rbd-provisioner created
    rolebinding.rbac.authorization.k8s.io/rbd-provisioner created
    serviceaccount/rbd-provisioner created
    deployment.extensions/rbd-provisioner created

    • Check RBD volume provisioner status and wait till it comes up in running state. You would see something like following:
    [root@ip-10-0-1-226 Ceph-RBD]# kubectl get pods -l app=rbd-provisioner -n kube-system
    NAME                               READY     STATUS    RESTARTS   AGE
    rbd-provisioner-857866b5b7-vc4pr   1/1       Running   0          16s

    • Once the provisioner is up, provisioner needs the admin key for the storage provision. You can run the following command to get the admin key:
    # ceph auth get-key client.admin
    AQDyWw9dOUm/FhAA4JCA9PXkPo6+OXpOj9N2ZQ==
    
    # kubectl create secret generic ceph-secret 
        --type="kubernetes.io/rbd" 
        --from-literal=key='AQDyWw9dOUm/FhAA4JCA9PXkPo6+OXpOj9N2ZQ==' 
        --namespace=kube-system

    • Let’s create a separate Ceph pool for Kubernetes and the new client key:
    # ceph --cluster ceph osd pool create kube 1024 1024
    # ceph --cluster ceph auth get-or-create client.kube mon 'allow r' osd 'allow rwx pool=kube'

    • Get the auth token which we created in the above command and create kubernetes secret for new client secret for kube pool.
    # ceph --cluster ceph auth get-key client.kube
    AQDabg9d4MBeIBAAaOhTjqsYpsNa4X10V0qCfw==
    
    # kubectl create secret generic ceph-secret-kube 
        --type="kubernetes.io/rbd" 
        --from-literal=key=”AQDabg9d4MBeIBAAaOhTjqsYpsNa4X10V0qCfw=='' 
        --namespace=kube-system

    • Now let’s create the storage class.
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: fast-rbd
    provisioner: ceph.com/rbd
    parameters:
      monitors: 10.0.1.118:6789, 10.0.1.227:6789, 10.0.1.172:6789
      adminId: admin
      adminSecretName: ceph-secret
      adminSecretNamespace: kube-system
      pool: kube
      userId: kube
      userSecretName: ceph-secret-kube
      userSecretNamespace: kube-system
      imageFormat: "2"
      imageFeatures: layering

    # kubectl create -f Ceph-RBD-StorageClass.yaml

    • We are all set now. We can test the Ceph-RBD by creating the PVC. After creating the PVC, PV will get created automatically.  Let’s create the PVC now:
    kind: PersistentVolumeClaim
    apiVersion: v1
    metadata:
      name: testclaim
    spec:
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi
      storageClassName: fast-rbd

    # kubectl create -f Ceph-RBD-PVC.yaml
    
    [root@ip-10-0-1-226 Ceph-RBD]# kubectl get pvc
    NAME      STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
    testclaim  Bound     pvc-c215ad98-95b3-11e9-8b5d-12e154d66096   1Gi        RWO            fast-rbd       2m

    • If you check pvc, you’ll find it shows that it’s been bounded with the pv which got created by storage class.
    • Let’s check the persistent volume
    [root@ip-10-0-1-226 Ceph-RBD]# kubectl get pv
    NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM             STORAGECLASS   REASON    AGE
    pvc-c215ad98-95b3-11e9-8b5d-12e154d66096   1Gi        RWO            Delete           Bound     default/testclaim   fast-rbd                 8m

    Till now we have seen how to use the block based storage i.e Ceph-RBD with kubernetes by creating the dynamic storage provisioner. Now let’s go through the process for setting up the storage using file system based storage i.e. CephFS.  

    CephFS and Kubernetes

    • Let’s create the provisioner and storage class for the CephFS.  Create the dedicated namespace for CephFS
    # kubectl create ns cephfs

    • Create the kubernetes secrete using the Ceph admin auth token
    # ceph auth get-key client.admin
    AQDyWw9dOUm/FhAA4JCA9PXkPo6+OXpOj9N2ZQ==
    
    # kubectl create secret generic ceph-secret-admin --from-literal=key="AQDyWw9dOUm/FhAA4JCA9PXkPo6+OXpOj9N2ZQ==" -n cephfs

    • Create the cluster role, role binding, provisioner
    kind: ClusterRole
    apiVersion: rbac.authorization.k8s.io/v1
    metadata:
      name: cephfs-provisioner
      namespace: cephfs
    rules:
      - apiGroups: [""]
        resources: ["persistentvolumes"]
        verbs: ["get", "list", "watch", "create", "delete"]
      - apiGroups: [""]
        resources: ["persistentvolumeclaims"]
        verbs: ["get", "list", "watch", "update"]
      - apiGroups: ["storage.k8s.io"]
        resources: ["storageclasses"]
        verbs: ["get", "list", "watch"]
      - apiGroups: [""]
        resources: ["events"]
        verbs: ["create", "update", "patch"]
      - apiGroups: [""]
        resources: ["services"]
        resourceNames: ["kube-dns","coredns"]
        verbs: ["list", "get"]
    ---
    kind: ClusterRoleBinding
    apiVersion: rbac.authorization.k8s.io/v1
    metadata:
      name: cephfs-provisioner
    subjects:
      - kind: ServiceAccount
        name: cephfs-provisioner
        namespace: cephfs
    roleRef:
      kind: ClusterRole
      name: cephfs-provisioner
      apiGroup: rbac.authorization.k8s.io
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: cephfs-provisioner
      namespace: cephfs
    rules:
      - apiGroups: [""]
        resources: ["secrets"]
        verbs: ["create", "get", "delete"]
      - apiGroups: [""]
        resources: ["endpoints"]
        verbs: ["get", "list", "watch", "create", "update", "patch"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: cephfs-provisioner
      namespace: cephfs
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: cephfs-provisioner
    subjects:
    - kind: ServiceAccount
      name: cephfs-provisioner
    ---
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: cephfs-provisioner
      namespace: cephfs
    ---
    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: cephfs-provisioner
      namespace: cephfs
    spec:
      replicas: 1
      strategy:
        type: Recreate
      template:
        metadata:
          labels:
            app: cephfs-provisioner
        spec:
          containers:
          - name: cephfs-provisioner
            image: "quay.io/external_storage/cephfs-provisioner:latest"
            env:
            - name: PROVISIONER_NAME
              value: ceph.com/cephfs
            - name: PROVISIONER_SECRET_NAMESPACE
              value: cephfs
            command:
            - "/usr/local/bin/cephfs-provisioner"
            args:
            - "-id=cephfs-provisioner-1"
          serviceAccount: cephfs-provisioner

    # kubectl create -n cephfs -f Ceph-FS-Provisioner.yaml

    • Create the storage class
    kind: StorageClass
    apiVersion: storage.k8s.io/v1
    metadata:
      name: cephfs
    provisioner: ceph.com/cephfs
    parameters:
        monitors: 10.0.1.226:6789, 10.0.1.205:6789, 10.0.1.82:6789
        adminId: admin
        adminSecretName: ceph-secret-admin
        adminSecretNamespace: cephfs
        claimRoot: /pvc-volumes

    # kubectl create -f Ceph-FS-StorageClass.yaml

    • We are all set now. CephFS provisioner is created. Let’s wait till it gets into running state.
    # kubectl get pods -n cephfs
    NAME                                 READY     STATUS    RESTARTS   AGE
    cephfs-provisioner-8d957f95f-s7mdq   1/1       Running   0          1m

    • Once the CephFS provider is up, try creating the persistent volume claim. In this step, storage class will take care of creating the persistent volume dynamically.
    kind: PersistentVolumeClaim
    apiVersion: v1
    metadata:
      name: claim1
    spec:
      storageClassName: cephfs
      accessModes:
        - ReadWriteMany
      resources:
        requests:
          storage: 1Gi

    # kubectl create -f Ceph-FS-PVC.yaml

    • Let’s check the create PV and PVC
    # kubectl get pvc
    NAME      STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
    claim1    Bound     pvc-a7db18a7-9641-11e9-ab86-12e154d66096   1Gi        RWX            cephfs         2m
    
    # kubectl get pv
    NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM            STORAGECLASS   REASON    AGE
    pvc-a7db18a7-9641-11e9-ab86-12e154d66096   1Gi        RWX            Delete           Bound     default/claim1   cephfs                   2m

    Conclusion

    We have seen how to integrate the Ceph storage with Kubernetes. In the integration, we covered ceph-rbd and cephfs. This approach is highly useful when your application is not clustered application and if you are looking to make it highly available.

  • A Primer To Flutter

    In this blog post, we will explore the basics of cross platform mobile application development using Flutter, compare it with existing cross-platform solutions and create a simple to-do application to demonstrate how quickly we can build apps with Flutter.

    Brief introduction

    Flutter is a free and open source UI toolkit for building natively compiled applications for mobile platforms like Android and iOS, and for the web and desktop as well. Some of the prominent features are native performance, single codebase for multiple platforms, quick development, and a wide range of beautifully designed widgets.

    Flutter apps are written in Dart programming language, which is a very intuitive language with a C-like syntax. Dart is optimized for performance and developer friendliness. Apps written in Dart can be as fast as native applications because Dart code compiles down to machine instructions for ARM and x64 processors and to Javascript for the web platform. This, along with the Flutter engine, makes Flutter apps platform agnostic.

    Other interesting Dart features used in Flutter apps is the just-in-time (JIT) compiler, used during development and debugging, which powers the hot reload functionality. And the ahead-of-time (AOT) compiler which is used when building applications for the target platforms such as Android or iOS, resulting in native performance.

    Everything composed on the screen with Flutter is a widget including stuff like padding, alignment or opacity. The Flutter engine draws and controls each pixel on the screen using its own graphics engine called Skia.

    Flutter vs React-Native

    Flutter apps are truly native and hence offer great performance, whereas apps built with react-native requires a JavaScript bridge to interact with OEM widgets. Flutter apps are much faster to develop because of a wide range of built-in widgets, good amount of documentation, hot reload, and several other developer-friendly choices made by Google while building Dart and Flutter. 

    React Native, on the other hand, has the advantage of being older and hence has a large community of businesses and developers who have experience in building react-native apps. It also has more third party libraries and packages as compared to Flutter. That said, Flutter is catching up and rapidly gaining momentum as evident from Stackoverflow’s 2019 developer survey, where it scored 75.4% under “Most Loved Framework, Libraries and Tools”.

     

    All in all, Flutter is a great tool to have in our arsenal as mobile developers in 2020.

    Getting started with a sample application

    Flutter’s official docs are really well written and include getting started guides for different OS platforms, API documentation, widget catalogue along with several cookbooks and codelabs that one can follow along to learn more about Flutter.

    To get started with development, we will follow the official guide which is available here. Flutter requires Flutter SDK as well as native build tools to be installed on the machine to begin development. To write apps, one may use Android Studios or VS Code, or any text editor can be used with Flutter’s command line tools. But a good rule of thumb is to install Android Studio because it offers better support for management of Android SDK, build tools and virtual devices. It also includes several built-in tools such as the icons and assets editor.

    Once done with the setup, we will start by creating a project. Open VS Code and create a new Flutter project:

    We should see the main file main.Dart with some sample code (the counter application). We will start editing this file to create our to-do app.

    Some of the features we will add to our to-do app:

    • Display a list of to-do items
    • Mark to-do items as completed
    • Add new item to the list

    Let’s start by creating a widget to hold our list of to-do items. This is going to be a StatefulWidget, which is a type of widget with some state. Flutter tracks changes to the state and redraws the widget when a new change in the state is detected.

    After creating theToDoList widget, our main.Dart file looks like this:

    /// imports widgets from the material design 
    import 'package:flutter/material.dart';
    
    void main() => runApp(TodoApp());
    
    /// Stateless widgets must implement the build() method and return a widget. 
    /// The first parameter passed to build function is the context in which this widget is built
    class TodoApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'TODO',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: TodoList(),
        );
      }
    }
    
    /// Stateful widgets must implement the createState method
    /// State of a stateless widget against has a build() method with context
    class TodoList extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => TodoListState();
    }
    
    class TodoListState extends State<TodoList> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Todo'),
          ),
          body: Text('Todo List'),
        );
      }
    }

    The ToDoApp class here extends Stateless widget i.e. a widget without any state whereas ToDoList extends StatefulWidget. All Flutter apps are a combination of these two types of widgets. StatelessWidgets must implement the build() method whereas Stateful widgets must implement the createState() method.

    Some built-in widgets used here are the MaterialApp widget, the Scaffold widget and AppBar and Text widgets. These all are imported from Flutter’s implementation of material design, available in the material.dart package. Similarly, to use native looking iOS widgets in applications, we can import widgets from the flutter/cupertino.dart package.

    Next, let’s create a model class that represents an individual to-do item. We will keep this simple i.e. only store label and completed status of the to-do item.

    class Todo {
      final String label;
      bool completed;
      Todo(this.label, this.completed);
    }

    The constructor we wrote in the code above is implemented using one of Dart’s syntactic sugar to assign a constructor argument to the instance variable. For more such interesting tidbits, take the Dart language tour.

    Now let’s modify the ToDoListState class to store a list of to-do items in its state and also display it in a list. We will use ListView.builder to create a dynamic list of to-do items. We will also use Checkbox and Text widget to display to-do items.

    /// State is composed all the variables declared in the State implementation of a Stateful widget
    class TodoListState extends State<TodoList> {
      final List<Todo> todos = List<Todo>();
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Todo'),
          ),
          body: Padding(
            padding: EdgeInsets.all(16.0),
            child: todos.length > 0
                ? ListView.builder(
                    itemCount: todos.length,
                    itemBuilder: _buildRow,
                  )
                : Text('There is nothing here yet. Start by adding some Todos'),
          ),
        );
      }
    
      /// build a single row of the list
      Widget _buildRow(context, index) => Row(
            children: <Widget>[
              Checkbox(
                  value: todos[index].completed,
                  onChanged: (value) => _changeTodo(index, value)),
              Text(todos[index].label,
                  style: TextStyle(
                      decoration: todos[index].completed
                          ? TextDecoration.lineThrough
                          : null))
            ],
          );
    
      /// toggle the completed state of a todo item
      _changeTodo(int index, bool value) =>
          setState(() => todos[index].completed = value);
    }

    A few things to note here are: private functions start with an underscore, functions with a single line of body can be written using fat arrows (=>) and most importantly, to change the state of any variable contained in a Stateful widget, one must call the setState method.

    The ListView.builder constructor allows us to work with very large lists, since list items are created only when they are scrolled.

    Another takeaway here is the fact that Dart is such an intuitive language that it is quite easy to understand and you can start writing Dart code immediately.

    Everything on a screen, like padding, alignment or opacity, is a widget. Notice in the code above, we have used Padding as a widget that wraps the list or a text widget depending on the number of to-do items. If there’s nothing in the list, a text widget is displayed with some default message.

    Also note how we haven’t used the new keyword when creating instances of a class, say Text. That’s because using the new keyword is optional in Dart and discouraged, according to the effective Dart guidelines.

    Running the application

    At this point, let’s run the code and see how the app looks on a device. Press F5, then select a virtual device and wait for the app to get installed. If you haven’t created a virtual device yet, refer to the getting started guide.

    Once the virtual device launches, we should see the following screen in a while. During development, the first launch always takes a while because the entire app gets built and installed on the virtual device, but subsequent changes to code are instantly reflected on the device, thanks to Flutter’s amazing hot reload feature. This reduces development time and also allows developers and designers to experiment more frequently with the interface changes.

    As we can see, there are no to-dos here yet. Now let’s add a floating action button that opens a dialog which we will use to add new to-do items.

    Adding the FAB is as easy as passing floatingActionButton parameter to the scaffold widget.

    floatingActionButton: FloatingActionButton(
      child: Icon(Icons.add),                                /// uses the built-in icons
      onPressed: () => _promptDialog(context),
    ),

    And declare a function inside ToDoListState that displays a popup (AlertDialog) with a text input box.

    /// display a dialog that accepts text
      _promptDialog(BuildContext context) {
        String _todoLabel = '';
        return showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                title: Text('Enter TODO item'),
                content: TextField(
                    onChanged: (value) => _todoLabel = value,
                    decoration: InputDecoration(hintText: 'Add new TODO item')),
                actions: <Widget>[
                  FlatButton(
                    child: new Text('CANCEL'),
                    onPressed: () => Navigator.of(context).pop(),
                  ),
                  FlatButton(
                    child: new Text('ADD'),
                    onPressed: () {
                      setState(() => todos.add(Todo(_todoLabel, false)));
                      /// dismisses the alert dialog
                      Navigator.of(context).pop();
                    },
                  )
                ],
              );
            });
      }

    At this point, saving changes to the file should result in the application getting updated on the virtual device (hot reload), so we can just click on the new floating action button that appeared on the bottom right of the screen and start testing how the dialog looks.

    We used a few more built-in widgets here:

    • AlertDialog: a dialog prompt that opens up when clicking on the FAB
    • TextField: text input field for accepting user input
    • InputDecoration: a widget that adds style to the input field
    • FlatButton: a variation of button with no border or shadow
    • FloatingActionButton: a floating icon button, used to trigger primary action on the screen

    Here’s a quick preview of how the application should look and function at this point:

    And just like that, in less than 100 lines of code, we’ve built the user interface of a simple, cross platform to-do application.

    The source code for this application is available here.

    A few links to further explore Flutter:

    Conclusion:

    To conclude, Flutter is  an extremely powerful toolkit to build cross platform applications that have native performance and are beautiful to look at. Dart, the language behind Flutter, is designed considering the nuances of user interface development and Flutter offers a wide range of built-in widgets. This makes development fun and development cycles shorter; something that we experienced while building the to-do app. With Flutter, time to market is also greatly reduced which enables teams to experiment more often, collect more feedback and ship applications faster.  And finally, Flutter has a very enthusiastic and thriving community of designers and developers who are always experimenting and adding to the Flutter ecosystem.