<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Robert Vojta on Medium]]></title>
        <description><![CDATA[Stories by Robert Vojta on Medium]]></description>
        <link>https://medium.com/@robertvojta?source=rss-f9e2afc9a25f------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*R23y9FG6Fm-vmvyRbMlhvw.jpeg</url>
            <title>Stories by Robert Vojta on Medium</title>
            <link>https://medium.com/@robertvojta?source=rss-f9e2afc9a25f------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 12:36:23 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@robertvojta/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Leaving Medium]]></title>
            <link>https://medium.com/robert-vojta/leaving-medium-bde793758cb2?source=rss-f9e2afc9a25f------2</link>
            <guid isPermaLink="false">https://medium.com/p/bde793758cb2</guid>
            <category><![CDATA[medium]]></category>
            <dc:creator><![CDATA[Robert Vojta]]></dc:creator>
            <pubDate>Thu, 07 Dec 2017 22:45:49 GMT</pubDate>
            <atom:updated>2017-12-08T06:52:11.737Z</atom:updated>
            <content:encoded><![CDATA[<p>I decided to leave Medium. I’m not quite happy with it, how it works and especially in the are of code highlighting (Gist — I do not want to edit my post in several places). Not saying Medium is bad, it just doesn’t suit my needs.</p><p>If you’re interested in my occasional posts about AWS, Python, iOS, Mac, …, you can continue reading them at <a href="https://robertvojta.com/">https://robertvojta.com</a>. It’s a Wordpress blog and you can follow me via:</p><ul><li><a href="https://robertvojta.com/feed/rss2">https://robertvojta.com/feed/rss2</a></li><li><a href="https://twitter.com/robertvojta">https://twitter.com/robertvojta</a></li></ul><p>Or replace rss2 with rss, atom or rdf.</p><p>All Medium posts were imported to the new blog. Also I asked Medium support to remove custom domain robertvojta.com from this publication. If you have bookmarks to my posts, don’t worry, I did setup redirects for all of them. They still work.</p><p>Cu elsewhere :)</p><p>P.S. Will keep this publication alive for a while before I trash it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bde793758cb2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/robert-vojta/leaving-medium-bde793758cb2">Leaving Medium</a> was originally published in <a href="https://medium.com/robert-vojta">Zrzka’s adventures</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Python IDE on the iPad Pro]]></title>
            <link>https://medium.com/robert-vojta/python-ide-on-the-ipad-pro-d8234cd3a86f?source=rss-f9e2afc9a25f------2</link>
            <guid isPermaLink="false">https://medium.com/p/d8234cd3a86f</guid>
            <category><![CDATA[apple]]></category>
            <category><![CDATA[ios-11]]></category>
            <category><![CDATA[working-copy]]></category>
            <category><![CDATA[pythonista]]></category>
            <category><![CDATA[ios]]></category>
            <dc:creator><![CDATA[Robert Vojta]]></dc:creator>
            <pubDate>Fri, 20 Oct 2017 11:53:52 GMT</pubDate>
            <atom:updated>2017-10-20T11:53:52.634Z</atom:updated>
            <content:encoded><![CDATA[<h4>Foreword</h4><p>My career is kind of colourful. I was working on aircraft systems, VoIP HW &amp; SW, developing Linux kernel drivers, Linux distributions, OpenOffice.org and many other low level (RTOS) &amp; high level (desktop applications) stuff. Linux nerd. I said enough one day. I wanted to <em>just work</em> and wasn’t willing to continue wasting my time with searching how to workaround this and that. Bye bye Linux, did spend nice 10 years with you. Not saying it was a bad experience. Nope, I learned a lot and enjoyed it. But …</p><p>Fast forward, I’m with Apple for more than 10 years now. Hmm, another 10 years, time to leave? Nope! Honestly, not excited as I was when I switched, but still pretty happy. Remember the first iPhone? iPad? Magical devices. Not mentioning first SDK. Exciting times. I did develop lot of applications for iPhones, iPads and for the desktop. Learned a lot about Objective-C, Objective-C runtime and all these Apple platforms.</p><p>I also bought every iPad Apple did release. But I didn’t know what to do with it. Kind of device for content consumers. I tried, really hard, to use it as a device where I can create something. But I always struggled, I always gave up and go back to my Mac. Apparently I was trying to use it for something it wasn’t designed for. Or it was, but iOS itself wasn’t ready for it.</p><p>My needs changed few months ago. I no longer work as a full time developer, but rather trying to lead people and working on new product specifications. Hard change. Lot of new things to learn.</p><p>Do I need to travel with my 15&quot; MacBook Pro for this kind of work? Am I willing to try iPad again? Yup, I am. Bought the iPad Pro 12.9&quot;, external keyboard and pencil few months ago. iOS 11, Files, drag &amp; drop, wonderful applications like <a href="https://workflow.is/">Workflow</a>, <a href="https://www.omnigroup.com/omnigraffle/ios/">OmniGraffle</a>, <a href="http://omz-software.com/editorial/index.html">Editorial</a>, <a href="https://ulyssesapp.com/">Ulysses</a>, <a href="https://mindnode.com/mindnode/ios">MindNode</a>, etc. I have to say that the iPad Pro makes sense as a device where I can create stuff finally. Took quite some time, but it seems it’s already here. Suits my needs perfectly.</p><p>What all this has to do with Python IDE on the iPad Pro? I’m no longer working on our products, but I didn’t left programming. And I never will. It’s quite refreshing to hide myself with all these devices in some place where no one can disturb me from time to time. We do use <a href="https://aws.amazon.com/">Amazon Web Services</a> a lot. I’m still kinda involved in scripting, support tools, … and we do write all these things in Python. Amazon provides nice package named <a href="https://boto3.readthedocs.io/en/latest/">boto3</a>:</p><blockquote>Boto is the Amazon Web Services (AWS) SDK for Python, which allows Python developers to write software that makes use of Amazon services like S3 and EC2. Boto provides an easy to use, object-oriented API as well as low-level direct service access.</blockquote><p>I wrote lot of tools, scripts, … leveraging boto3 power to maintain our AWS services. Also wrote some <a href="https://www.djangoproject.com/">django</a> applications for internal use. Can I do this on my iPad Pro? Quite some effort, but yes, I can! And now, you&#39;re finally going to learn how.</p><h4>Pythonista for iOS</h4><p>Hats off to <a href="http://twitter.com/olemoritz">Ole</a> for writing <a href="http://omz-software.com/pythonista/index.html">Pythonista for iOS</a>. Seriously, tremendous amount of work. Quote from the Pythonista website:</p><blockquote>Pythonista is a complete development environment for writing Python™ scripts on your iPad or iPhone. Lots of examples are included — from games and animations to plotting, image manipulation, custom user interfaces, and automation scripts.</blockquote><blockquote>In addition to the powerful standard library, Pythonista provides extensive support for interacting with native iOS features, like contacts, reminders, photos, location data, and more.</blockquote><p>Pythonista supports Python 2.7, 3.5 (beta includes 3.6), standard library, lot of bundled packages and neat support for native iOS features.</p><p>Release cycle of Pythonista is quite long. Version 3.0 was released an year ago, version 3.1 just a few days ago. This release cycle leads to a situation, where bundled packages can be outdated and native iOS features doesn’t support latest &amp; greatest stuff — drag &amp; drop, Files.app, etc.</p><p>What can be done about this? There’s project named <a href="https://github.com/ywangd/stash">StaSh — Shell for Pythonista</a>. Do I need shell you say? Bet you do. StaSh does contain useful commands like pip, which allows you to install additional packages from <a href="https://pypi.python.org/pypi">PyPI</a>. Not all packages are supported. Package must be <em>pure Python</em>, because of the way how binaries, signatures, … work on iOS. It&#39;s more complicated, but I&#39;m not going to explain it here. Just try it and you’ll see if your package will be installed or not.</p><p>Remember the first iPhone OS SDK? First apps? Did you ever think this will be possible? And we just started.</p><p>You should really <a href="https://itunes.apple.com/us/app/pythonista-3/id1085978097?ls=1&amp;mt=8">buy</a> Pythonista if you’re into Python.</p><h4>Dark side of the moon</h4><p>You’re excited, you started to love Pythonista, but you slowly do realize that you miss something. Keyboard shortcuts, features like <em>Jump to definition</em>, <em>Open quickly</em> and lot of other things. Stuff which makes great IDE great.</p><p>I started <a href="https://github.com/omz/Pythonista-Issues/issues/created_by/zrzka?page=1&amp;q=is%3Aopen+is%3Aissue+author%3Azrzka">filling issues</a> as a first thing. There’s more than 330 open issues. Hmm, how quickly will Ole solve them? Am I willing to wait weeks, months, …? Isn’t my use case minor one? Will they ever be fixed? Lot of questions. I’m a problem solver guy, so, I decided to fix it by myself. That’s how the <a href="http://blackmamba.readthedocs.io/">Black Mamba</a> project started.</p><h4>Prerequisites</h4><p>I assume that you’re familiar with following topics:</p><ul><li><a href="https://developer.apple.com/documentation/objectivec/objective_c_runtime?language=objc">Objective-C Runtime</a> — messaging, signatures and type encoding,</li><li><a href="https://developer.apple.com/documentation/uikit/understanding_event_handling_responders_and_the_responder_chain?language=objc">Event handling, responders and the responder chain</a> — <a href="https://developer.apple.com/documentation/uikit/uiresponder?language=objc">UIResponder</a>, <a href="https://developer.apple.com/documentation/uikit/uikeycommand?language=objc">UIKeyCommand</a> and <a href="https://developer.apple.com/documentation/uikit/uievent?language=objc">UIEvent</a>.</li></ul><p>Also I highly recommend to look at <a href="https://docs.python.org/3.6/library/ctypes.html">ctypes</a> and <a href="http://omz-software.com/pythonista/docs/ios/objc_util.html">objc_util</a> in advance.</p><h4>Keyboard shortcuts</h4><p>Pythonista provides small amount of shortcuts and I can’t work without them. Still consider screen tapping as a slow thing. Let’s add some of them. We have two basic requirements:</p><ul><li>be at the end of the responder chain, just to avoid clashes with Pythonista view controllers shortcuts,</li><li>have them working even if the view controller hierarchy changes over time.</li></ul><p>It seems that the keyCommands property of the UIApplication perfectly suits our needs. When compared to the UIViewController, there’s no addKeyCommand: and we have to found another way how to modify keyCommands result. Time to poke Objective-C runtime, especially <a href="https://developer.apple.com/documentation/objectivec/1418901-class_addmethod?language=objc">class_addMethod</a> and <a href="https://developer.apple.com/documentation/objectivec/1418901-class_addmethod?language=objc">method_exchangeImplementations</a> functions. First one allows us to add new methods and second one allows us to exchange their implementations. It’s called swizzling. We will need to use these functions more than once, so, here are Python wrappers:</p><ul><li><a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/util/runtime.py#L16">add_method</a> — adds new method to the class,</li><li><a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/util/runtime.py#L16">swizzle</a> — adds new method to the class and exchange implementations with the original one.</li></ul><p>Ready for swizzling? Let’s write custom keyCommands function in Python:</p><pre>def _blackmamba_keyCommands(_self, _cmd):<br>    obj = ObjCInstance(_self)<br>    commands = list(obj.originalkeyCommands() or [])<br>    commands.extend(_key_commands)<br>    return ns(commands).ptr</pre><p>Simple one, which gets the original list of key commands and adds our ones from the _key_commands list (module global variable). We do not need to add new method explicitly, our swizzle function does it automatically:</p><pre>swizzle(&#39;UIApplication&#39;, &#39;keyCommands&#39;, _blackmamba_keyCommands)</pre><p>What happened? A picture is worth a thousand words:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*Ln44x0jYqIDpwrbSke1jeg.png" /></figure><p>The only remaining thing now is to introduce function, which allows us to register keyboard shortcuts — <a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/uikit/keyboard.py#L305">register_key_command</a>. Signature of this function follows.</p><pre>def _register_key_command(input, modifier_flags, function, title=None)</pre><ul><li>input - it can be any string (like letter a for example) or <a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/uikit/keyboard.py#L194">UIKeyInput</a> enum, which does contain values for special keys like left arrow.</li><li>modifier_flags - it can be int (if you know the correct value) or you can use <a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/uikit/keyboard.py#L103">UIKeyModifier</a> enum.</li><li>function - Python function, no arguments, to call when user presses the shortcut.</li><li>title - optional str, discoverability title.</li></ul><p>How to print <em>Hallo</em> with Cmd H?</p><pre>from blackmamba.uikit.keyboard import (<br>   register_key_command, UIKeyModifier<br>)<br>def hallo():<br>   print(&#39;Hallo&#39;)<br>register_key_command(<br>   &#39;h&#39;,<br>   UIKeyModifier.command,<br>   hallo<br>)</pre><p>Passed function hallo is wrapped with a function, which does meet Objective-C runtime specs.</p><pre>def key_command_action(_sel, _cmd, sender):<br>        function()</pre><p>Based on input and modifier_flags, method name is generated. blackMambaHandleKeyCommandH: in this case.</p><p>blackMambaHandleKeyCommandH: method is added to the UIApplication with implementation pointing to the key_command_action. UIKeyCommand object is created and stored in the _key_commands global list.</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*NLs4PnNMWL7uHDyKcGrOTg.png" /></figure><p>And that’s it. We have a way how to register custom keyboard shortcut, assign it to a function written in Python.</p><p>Pretty awesome and I can’t still believe it’s possible.</p><h4>Keyboard events</h4><p>Imagine you have a dialog and you would like to use keyboard shortcuts for it as well. Like arrow keys to change selection, enter to confirm selection, Cmd . to close dialog, etc. Global shortcuts can be reused for this task, but it will bring another sort of issues. We will be forced to remember which dialog is active, pass events to it from our shortcut handler, forget the dialog when it’s closed, … Sounds complicated to me.</p><p>Let’s solve this task in a similar way, by swizzling handleKeyUIEvent: (UIApplication). This method receives physical keyboard events (type = 4) and we can handle them there.</p><blockquote>This is private API, don’t do this in your applications.</blockquote><p>Because it’s very similar to global keyboard shortcuts, here’re just links to specific implementations:</p><ul><li><a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/uikit/keyboard.py#L384-L398">_blackmamba_handleKeyUIEvent</a> — our implementation of handleKeyUIEvent:,</li><li><a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/uikit/keyboard.py#L402-L412">register_key_event_handler</a> — event registration,</li><li><a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/uikit/keyboard.py#L431">unregister_key_event_handler</a> — event unregistration.</li></ul><p>And an example how to <a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/uikit/picker.py#L227-L259">register</a> and <a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/uikit/picker.py#L261-L263">unregister</a> keyboard events.</p><h4>Drag &amp; drop</h4><p>There was no drag &amp; drop support prior to Pythonista (311013, beta). I desperately did want to use it and wrote another script — <a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/script/drag_and_drop.py">drag_and_drop.py</a>.</p><p>This script demonstrates how to create custom classes, classes that implements protocols like UITableViewDragDelegate and how to use blocks. I think it’s pretty self explanatory except one thing.</p><p>There’s experimental support of blocks in Pythonista. You can write a Python function and create block from it. Here’s an example:</p><pre>def block_imp(_cmd):<br>   print(&#39;Hallo&#39;)<br>block = ObjCBlock(<br>   block_imp,<br>   restype=ctypes.c_void_p,<br>   argtypes=[ctypes.c_void_p, ctypes.c_void_p]<br>)</pre><p>If you need to pass block as an argument somewhere, you can just pass block variable now and that’s it. Works. But what if you have to implement block, which receives another block as an argument. There’s no support for it in Pythonista.</p><p>I wrote the <a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/util/runtime.py#L76-L165">ObjCBlockPointer</a> class, which can be initialised with the block pointer. And then you can call it. Here’s <a href="https://github.com/zrzka/blackmamba/blob/v1.3.1/blackmamba/script/drag_and_drop.py#L67">an example</a> how to use this class. Drag session delegate, which is supposed to load data and then call another block when the operation completes. It’s achieved via ObjCBlockPointer class.</p><h4>Conclusion</h4><p>Do you need additional packages like me? Just install <a href="https://github.com/ywangd/stash">StaSh</a> and use pip. I did install django, boto3 and many other packages. Do you miss some functionality in Pythonista? Install <a href="http://blackmamba.readthedocs.io/">Black Mamba</a>.</p><p>I like Pythonista for iOS a lot. I do use it almost on a daily basis even when it’s not perfect (nothing’s perfect) and lacks some features. But because it’s Python IDE, we have an access to the Objective-C runtime, nothing can stop you from enhancing it on your own. Like I did. See the <a href="http://blackmamba.readthedocs.io/en/stable/gallery.html">gallery</a> of <a href="https://github.com/zrzka/blackmamba/tree/v1.3.1/blackmamba/script">scripts</a> I wrote.</p><p>Can the iPad Pro replace my desktop computer? Yup, it can. There’re still some edge cases where I rather use my MacBook Pro, but the number of them is very low.</p><p>Did you ever think that it will be possible to do these kind of things? In an App Store application, no jailbreak, … Me not, never, until now. And I can’t still kind of believe it.</p><p>Learn or die and remember, <strong>sky is the limit</strong> :)</p><p>Again, many thanks to <a href="https://twitter.com/olemoritz">Ole</a> for writing this wonderful piece of software! Please, <a href="https://itunes.apple.com/us/app/pythonista-3/id1085978097?ls=1&amp;mt=8">buy</a> it to support Ole’s ongoing effort to make it better.</p><h4>Links</h4><ul><li><a href="http://omz-software.com/pythonista/">Pythonista for iOS</a></li><li><a href="https://workingcopyapp.com/">Working Copy for iOS</a></li><li><a href="https://github.com/ywangd/stash">StaSh</a></li><li><a href="http://blackmamba.readthedocs.io/">Black Mamba documentation</a></li><li><a href="https://github.com/zrzka/blackmamba">Black Mamba repository</a></li></ul><h4>Django notes</h4><p>If you’d like to run Django application on your iPad Pro, don’t forget to pass following arguments:</p><pre>dir = os.path.abspath(os.path.join(os.path.dirname(path), &#39;../application&#39;))<br>sys.path.append(dir)<br># import Django execute_from_command_line<br>arguments = list(sys.argv)<br>arguments.append(&#39;runserver&#39;)<br>arguments.append(&#39;--noreload&#39;)<br>arguments.append(&#39;--nothreading&#39;)<br>arguments.append(&#39;-v&#39;)<br>arguments.append(&#39;0&#39;)<br>logging.disable(logging.CRITICAL)<br>try:<br>    execute_from_command_line(arguments)<br>except KeyboardInterrupt:<br>    logging.disable(logging.NOTSET)<br>    sys.path.remove(dir)</pre><p>Why these arguments? Django is very talkative and it is constantly filling your console with messages. Every single time there’s a new message, Pythonista will reveal console, tab with your app becomes inactive and you have to tap on it to reveal your application. Kind of … That’s the reason for -v 0 and disabled logging.</p><p>No threading &amp; reloading is here, because Pythonista supports only one Python thread.</p><p>Still some limitations, but better than nothing. Otherwise it works perfectly.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d8234cd3a86f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/robert-vojta/python-ide-on-the-ipad-pro-d8234cd3a86f">Python IDE on the iPad Pro</a> was originally published in <a href="https://medium.com/robert-vojta">Zrzka’s adventures</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AWS journey — API Gateway & Lambda & VPC performance]]></title>
            <link>https://medium.com/robert-vojta/aws-journey-api-gateway-lambda-vpc-performance-452c6932093b?source=rss-f9e2afc9a25f------2</link>
            <guid isPermaLink="false">https://medium.com/p/452c6932093b</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[vpc]]></category>
            <category><![CDATA[aws-api-gateway]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <dc:creator><![CDATA[Robert Vojta]]></dc:creator>
            <pubDate>Mon, 31 Oct 2016 17:34:16 GMT</pubDate>
            <atom:updated>2016-11-02T22:06:08.839Z</atom:updated>
            <content:encoded><![CDATA[<p>We decided to hide some EC2 instances in private subnets (VPC). They’re accessible via bastion hosts or via API (API Gateway &amp; Lambda). Works well, but there’s one weird issue — cold lambda start time is over 10s sometimes. We experienced even 15s. This is not acceptable and I’m seeking for answers to my questions like:</p><ul><li>Does increased memory size help?</li><li>Is this huge cold start time VPC related?</li><li>Is there a difference when we do use another language?</li><li>When the lambda container is reused?</li><li>Does some kind of keep alive requests help?</li></ul><p>Internet is full of answers, but they’re not clear. Wild guesses. So, I decided to quickly hack <a href="https://github.com/purposefly/lambda-performance">benchmark</a> to get more accurate (still not precise) answers.</p><p>Scroll down if you’re not interested in details, but want to see results only.</p><h3>Lambda</h3><h4>Limits</h4><p>Always learn limits of your <a href="http://docs.aws.amazon.com/lambda/latest/dg/limits.html">enemy</a>. What’s interesting for now is number of concurrent executions. Default limit is 100.</p><h4>Cold vs Hot</h4><p>Lambda function runs in a sandbox environment; container. Containers can be reused. Container created for lambda A can’t be reused for lambda B, just for lambda A. There’s no clear answer when they’re reused. But I would like to know.</p><p>Every container has ephemeral disk capacity (0.5GB) mounted to <em>/tmp</em>. Whatever lambda stores there, it stays there if container is reused. My reuse test is very simple. Container is reused if file <em>/tmp/perf.txt</em> exists. If not, lambda is going to create it. Check <a href="https://github.com/purposefly/lambda-performance/blob/master/lambda/python/perf_handler.py">Python</a>, <a href="https://github.com/purposefly/lambda-performance/blob/master/lambda/js/perf_handler.js">JavaScript</a> and <a href="https://github.com/purposefly/lambda-performance/blob/master/lambda/cljs/src/performance/core.cljs">ClojureScript</a> lambda function handlers.</p><p>More info about container reuse on the <a href="https://aws.amazon.com/blogs/compute/container-reuse-in-lambda/">AWS blog</a>. Old, but enough to get a clue.</p><h4>Variants</h4><p>I would like to see if there’s a difference between languages, memory sizes and VPC [not] set. I prepared several variants:</p><ul><li>Lambda functions in Python, JavaScript and ClojureScript</li><li>Each configured with 128, 256, 512, 768, 1024, 1280, 1536MB of memory</li><li>Each with and without VPC</li></ul><p>All possible combinations, 42 lambda functions.</p><p>NOTE: There’s no way how configure CPU for lambda function. It’s tightly tied with memory size. Bigger memory size, faster CPU. This is the reason why I made different memory variants as well.</p><h4>API Gateway</h4><h4>Limits</h4><p>Again, your enemy <a href="http://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html">limits</a>. Requests per seconds default limit is 1,000. Burst 2,000. This limit is per account; don’t kill your other APIs with stress testing.</p><h3>VPC</h3><h4>Limits</h4><p>We’re interested in <a href="http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Appendix_Limits.html#vpc-limits-enis">ENI limits</a>. You can have 350 network interfaces per account. And here are <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI">limits per instance</a>. I’ve got t1.micro for the benchmark purpose, so, the limit is 2.</p><h4>Lambda</h4><p>Lambda function is not able to access VPC resources by default. You have to set at least one subnet and at least one security group from the same VPC to allow it to access VPC resources.</p><p>NOTE: If you’re using ClojureScript, there’s <a href="https://github.com/nervous-systems/cljs-lambda/pull/48">pull request</a> with VPC support. Already merged, 0.6.3-SNAPSHOT published, you can test it.</p><p>Time to learn more about <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html">ENI</a>. It’s a virtual network interface attached to EC2 instance in your VPC. ENI can be created, deleted, attached, detached, … How does it work with Lambda container? ENI is created (or reused) during lambda/container initialisation process. This ENI is used within lambda container to allow it to communicate with your instance. Lambda ends, container is suspended, but ENI is still there. ENI is deleted when the lambda container is deleted. It takes some time and I want to know when approximately.</p><p>Also think about 350 ENIs limit when you’re designing your infrastructure. If there’s no ENI available, lambda ends with internal server error and API Gateway returns 500:</p><blockquote>Lambda was not able to create an ENI in the VPC of the Lambda function because the limit for Network Interfaces has been reached.</blockquote><p>Lambda role must have policy allowing to create ENI, delete ENI, … Do not create your own policies, but stick with the service one created and maintained by the AWS guys — <em>AWSLambdaVPCAccessExecutionRole</em>:</p><blockquote>arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole</blockquote><p>BTW I tried to create my own policy. I did forget to add delete interface action and drained ENI pool pretty quickly. Managed to create 352 ENIs (limit is 350).</p><h3>Benchmark</h3><h4>Methodology</h4><p>None. I just wanted to see some numbers and I quickly hacked <a href="https://github.com/purposefly/lambda-performance/blob/master/benchmark/benchmark.py">Python script</a> which sleeps for 0, 5, 10, 15, 30, 60, 300, 600, 900, 1800, 2700 and 3600 seconds. And for every sleep cycle:</p><ul><li>Sequentially benchmarks predefined endpoints (<em>/js/128</em>, <em>/js/256</em>, …)</li><li>Fires 30 requests per endpoint via 10 workers (parallel requests)</li><li>Gathers each request-response duration and container reuse info</li></ul><p>It fires 15,120 requests in total and it runs for more than two hours. Sleep is there to approximate time when the container is reused and when it isn’t.</p><p>Should be enough to get some data and it also shouldn’t trigger throttling on the AWS side.</p><h4>Technical</h4><p>Always run <a href="https://github.com/purposefly/lambda-performance/blob/master/redeploy.sh">redeploy.sh</a> script before you run this benchmark. Lambda functions are redeployed and all containers are trashed. You can check that the first endpoint requests (up to 10) should return no container reuse.</p><h4>Output</h4><p>Script generates CSV file with following values:</p><ul><li><em>sleep</em> — in which sleep cycle (group) were requests issued (<em>seconds</em>)</li><li><em>start</em> — exact date and time when the batch of 30 requests was fired (IOW first request start date time from the particular batch)</li><li><em>path</em> — API Gateway path</li><li><em>lang</em> — lambda function language (<em>js</em>, <em>python</em>, <em>cljs</em>)</li><li><em>vpc </em>— lambda with VPC? (<em>1</em> yes, <em>0</em> no)</li><li><em>memory</em> — lambda memory size (<em>MB</em>)</li><li><em>workers </em>— number of workers in this batch (parallel requests)</li><li><em>min</em> — fastest request-response time (<em>milliseconds</em>)</li><li><em>max</em> — slowest request-response time (<em>milliseconds</em>)</li><li><em>mean</em> — mean request-response time (<em>milliseconds</em>)</li><li><em>d1, d2, …, d30</em> — first, second, … request-response duration (<em>milliseconds</em>)</li><li><em>r1, r2, …, r30</em> — first, second, … request-response lambda container reuse info (<em>1</em> reused, <em>0</em> not reused)</li></ul><h4>Environment conditions</h4><p>No lab, no equal conditions, … Do not compare absolute values. I’m benchmarking it before lunch, in the afternoon, during night, … On my VDSL lines, over the air (LTE), at home, at work, … The only thing which is constant is region (eu-west-1) and benchmark settings (endpoints, memory sizes, VPC, number of requests per endpoint and number of workers).</p><h3>Analysis</h3><p>We’re going to focus on <em>vdsl-home-morning</em> data set for the purpose of this analysis.</p><p>Script fired 15,120 requests. Simple sort in Numbers and we already know that the slowest response took 13,106ms (13s) and the fastest one 191ms. There’re …</p><ul><li>177 requests (1.2%) slower than 10s</li><li>403 requests (2.7%) slower than 5s</li><li>539 requests (3.6%) slower than 2s</li><li>723 requests (4.8%) slower than 1s</li><li>1,921 requests (8.5%) slower than 0.5s</li></ul><p>Pretty big deviation and kind of disappointment. CSV table, raw data, … all these things are not clear. Let’s dive in, use R, plot some charts, check what’s wrong and try to fix it.</p><p>Following part was done with cooperation with <a href="https://medium.com/@tomas.bouda">Tomáš Bouda</a>. Our math, stats, … magician. Many thanks Tobbi!</p><h4>Overview</h4><p>First thing to check is histogram. It shows that lot of requests are quick, but there’s something weird going on around 10 seconds.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2avzvdJXbQKt-903m6npbw.png" /></figure><p>Let’s zoom in left side (limit time to 150–500ms) to check typical request duration. It’s somewhere in the 200–300ms range. Which is not super quick, but it’s not 10 seconds or more.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*76fMxWg7NxLFV17x50D2IA.png" /></figure><p>Histogram is not going to help us more, we’re going to try box plot. Much better information.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vkM2b6zxHpzJhr83OZRktw.png" /></figure><p>Left half is without VPC. Right half is with VPC. As you can see, we have lot of requests with duration greater than 5s. To be more precise, with duration in the 7.5-12.5s range.</p><p>We have lot of input variables and we have to get rid of them. Let’s limit time to 200-500ms in this box plot.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QXhALNJTNP3HyT2Fgwd8ZQ.png" /></figure><p>What does it say? If everything works as expected, there’s no difference between languages and memory sizes.</p><p>We can safely ignore language and memory size variables. What do we have now? VPC and lambda container reuse variables.</p><h4>Lambda containers reuse</h4><p>X axis shows when requests were fired and Y axis shows their duration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jX0sTrQaA5prrrdCE3fHeg.png" /></figure><p>First batch has several durations around 10s. That’s “okay”, because lambda functions were redeployed before benchmark and there were no containers to reuse. But what we can see is that containers weren’t reused about one hour after the benchmark start.</p><p>Let’s find more precise time and use more colours. Sleep time of 900s (15m) looks okay. Sleep time of 1800s (30m) doesn’t. We can say that containers are not reused after 15 minutes (rough estimate).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PoV_9_jVwOcKeJHCMx8LQQ.png" /></figure><p>Some numbers and explanation from Tobbi:</p><blockquote>To ensure ourselves let’s check the numbers. 2nd and 3rd columns contain sample mean and sample standard deviation. We can see that most of the means are below 300ms. The bold rows have higher means since right-skewed distributions tend to pull mean off.</blockquote><blockquote>4th to 6th columns contain a probability that call remains below given time limit using Gaussian distribution, e.g. norm.1000 represents probability the call is below 1000ms. Let’s ignore the bold rows since these distributions are bi-modal and not Gaussian.</blockquote><blockquote>The last two columns contain empirical probabilities based on the test data, e.g. emp.500 show the probability the call remains under 500ms.</blockquote><blockquote>As we can see, we have pretty reasonable times (below 1 second) for up to 900s gaps.</blockquote><pre>sleep  mean    sd norm.500 norm.1000 norm.2000 emp.500 emp.1000<br> <strong>   0   939  2324    0.425     0.510     0.676   0.816    0.906</strong><br>    5   282    69    0.999     1.000     1.000   0.999    0.999<br>   10   282    82    0.996     1.000     1.000   0.991    0.998<br>   15   277    43    1.000     1.000     1.000   0.998    1.000<br>   30   283    74    0.998     1.000     1.000   0.990    0.998<br>   60   275    38    1.000     1.000     1.000   1.000    1.000<br>  300   287   123    0.957     1.000     1.000   0.985    0.998<br>  600   293   204    0.843     1.000     1.000   0.960    0.991<br>  900   294   153    0.909     1.000     1.000   0.933    0.994<br> <strong>1800   618  1757    0.473     0.586     0.784   0.867    0.960<br> 2700  1423  2945    0.377     0.443     0.578   0.756    0.832<br> 3600  1652  3089    0.355     0.416     0.545   0.679    0.751</strong></pre><h4>VPC</h4><p>Last thing we have to find is how VPC configuration influences all these durations. We’re lucky with another box plot.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hloOg_iHWdrPcq4pZHcjSQ.png" /></figure><p>X axis shows if the lambda container was (1) or wasn’t (0) reused. Y axis shows the request duration.</p><p>There’s no difference if container is reused. VPC can be set or not. Huge difference is when the container is not reused.</p><p>We can say that these unacceptable durations come from lambda functions with VPC when their containers are not reused.</p><h4>Keep alive containers</h4><p>We already found that these containers are not reused after ~15 minutes. I tried to keep them alive with quickly hacked <a href="https://github.com/purposefly/lambda-performance/blob/master/benchmark/keep_alive.py">keep alive</a> script.</p><p>This script fires 10 requests with 10 workers per endpoint to keep alive 10 containers. That’s because our benchmark script also do use 10 workers. These requests are fired in 15 minutes interval. What happened?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*937h3nnsLf9C7U7jiRzERQ.png" /></figure><p>Third data set (<em>ka-15-r-10-vdsl-home-afternoon</em>, keep alive time interval 15m, 10 containers) has one duration just below 5s and rest is in the 0-2.5s range.</p><p>Let’s look at the <em>ka-15-r-10-vdsl-home-afternoon</em> data set directly.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dGbFViQi-4hW5d5XfZPjbA.png" /></figure><p>Huge improvement. 10s requests are gone. Compare against <em>vdsl-home-morning</em> data set.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jX0sTrQaA5prrrdCE3fHeg.png" /></figure><p>We can say victory ;-) Not victory actually, but huge progress at least.</p><h3>Conclusion</h3><p>Be aware that our testing lambda functions are very simple. They just returns lambda container reuse info. These numbers and findings can differ if your lambda has lot of dependencies, does lot of other things, etc.</p><p>Based on these facts, we can say that:</p><ul><li>Lambda function language (Python, ClojureScript or JavaScript) has no influence on lambda container initialisation time</li><li>Memory size has no influence on lambda container initialisation time</li><li>VPC has huge impact on lambda container initialisation time when the lambda container is not reused, but have no impact at all (or negligible) when the lambda container is reused</li><li>Lambda containers are not reused after ~15 minutes</li><li>If we issue keep alive requests in the 15 minutes interval, very long request durations (&gt; 5s) are rare</li></ul><p>Recommendation?</p><p>Use <a href="https://aws.amazon.com/cloudwatch/">CloudWatch</a> to issue keep alive functions in the 15 minutes interval if you’re using VPC, API Gateway &amp; Lambda functions.</p><p>UPDATE: Configurable keep alive function in Python, CloudWatch event rule and CloudFormation template can be found <a href="https://gist.github.com/zrzka/47c224effa76d45135b1790a7d063c04">here</a>.</p><p>This will not solve your issue completely, but will help a lot. Especially if your lambda function is rarely used and containers are not reused. It’s a nightmare to explain to your user that he has to wait for 15s to get some response from your application.</p><p>Not sure if AWS guys will be happy, but what else can we do?</p><p>Do you want to play with this benchmark, roll your own analysis, …? Here’s the <a href="https://github.com/purposefly/lambda-performance">repository link</a> with all lambda functions, scripts, data sets and HTML outputs.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=452c6932093b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/robert-vojta/aws-journey-api-gateway-lambda-vpc-performance-452c6932093b">AWS journey — API Gateway &amp; Lambda &amp; VPC performance</a> was originally published in <a href="https://medium.com/robert-vojta">Zrzka’s adventures</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AWS journey — EC2 Container Service]]></title>
            <link>https://medium.com/robert-vojta/aws-journey-ec2-container-service-3ae12187cf35?source=rss-f9e2afc9a25f------2</link>
            <guid isPermaLink="false">https://medium.com/p/3ae12187cf35</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[docker]]></category>
            <dc:creator><![CDATA[Robert Vojta]]></dc:creator>
            <pubDate>Thu, 20 Oct 2016 21:21:15 GMT</pubDate>
            <atom:updated>2016-10-20T21:27:32.365Z</atom:updated>
            <content:encoded><![CDATA[<p>We were using (and we’re still it using for some services) Docker Cloud. Main problem with DC is that they can’t handle private subnets. EC2 instance must be in the public subnet and that’s not what we want.</p><p>Hooray (irony), we have to switch to the EC2 Container Service. Try to explain this to our business department. Additional time for infrastructure. Can handle it, but it’s tough sometimes.</p><h3>Docker images repository</h3><h4>Cloud Formation Template</h4><p>This step is optional, but we’re trying to leverage as many as possible AWS services. You need a repository for your Docker images. And because I like Cloud Formation, here’s the template:</p><pre>&quot;AthenaRepository&quot;: {<br>  &quot;Type&quot;: &quot;AWS::ECR::Repository&quot;,<br>  &quot;Condition&quot;: &quot;DevelopmentEnv&quot;,<br>  &quot;Properties&quot;: {<br>    &quot;RepositoryName&quot; : &quot;athena&quot;,<br>    &quot;RepositoryPolicyText&quot; : {<br>      &quot;Version&quot;: &quot;2008-10-17&quot;,<br>      &quot;Statement&quot;: [<br>        {<br>          &quot;Sid&quot;: &quot;ProductionAccountPullAccess&quot;,<br>          &quot;Effect&quot;: &quot;Allow&quot;,<br>          &quot;Principal&quot;: {<br>            &quot;AWS&quot;: &quot;arn:aws:iam::$ANOTHER-ACCOUNT-ID:root&quot;<br>          },<br>          &quot;Action&quot;: [<br>            &quot;ecr:GetDownloadUrlForLayer&quot;,<br>            &quot;ecr:BatchGetImage&quot;,<br>            &quot;ecr:BatchCheckLayerAvailability&quot;<br>          ]<br>        }<br>      ]<br>    }<br>  }<br>}</pre><p>Why <em>RepositoryPolicyText</em>? We have several AWS accounts, but we want just one repository. This policy gives pull access to <em>$ANOTHER-ACCOUNT-ID</em> account.</p><p>Why <em>DevelopmentEnv</em> condition? Same reason, just one repo per several AWS accounts. We do use same templates for all accounts. They’re parametrised.</p><h4>Push images</h4><p>There’s nothing new. Just replace <em>docker login …</em> with <em>$(aws ecr get-login)</em>.</p><pre><em>$(aws ecr get-login)</em></pre><pre><em>docker </em>build -t athena .</pre><pre><em>docker </em>tag athena $AWS_ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/athena:build-$TRAVIS_BUILD_NUMBER</pre><pre><em>docker </em>push $AWS_ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/athena:build-$TRAVIS_BUILD_NUMBER</pre><h3>EC2 instance</h3><h4>Cluster</h4><p>First of all, you need cluster. There’s no way how to deploy your services without it. Cloud Formation template:</p><pre>&quot;ECSCluster&quot; : {<br>  &quot;Type&quot; : &quot;AWS::ECS::Cluster&quot;,<br>  &quot;Properties&quot; : {<br>    &quot;ClusterName&quot; : &quot;Athena&quot;<br>  }<br>}</pre><p>Now we have <em>Athena</em> cluster. But it doesn’t contain EC2 instance(s). How to assign them? Fun is coming.</p><h4>Role</h4><p>We have to set EC2 instance profile, which can contain roles. And one of our roles must allow communication with EC2 Container Service.</p><pre>&quot;ECSRolePolicy&quot;: {<br>  &quot;Type&quot;: &quot;AWS::IAM::ManagedPolicy&quot;,<br>  &quot;Properties&quot;: {<br>    &quot;PolicyDocument&quot;: {<br>      &quot;Version&quot;: &quot;2012-10-17&quot;,<br>      &quot;Statement&quot;: [<br>        {<br>          &quot;Effect&quot;: &quot;Allow&quot;,<br>          &quot;Action&quot;: [<br>            &quot;ecr:BatchCheckLayerAvailability&quot;,<br>            &quot;ecr:BatchGetImage&quot;,<br>            &quot;ecr:DescribeRepositories&quot;,<br>            &quot;ecr:GetAuthorizationToken&quot;,<br>            &quot;ecr:GetDownloadUrlForLayer&quot;,<br>            &quot;ecr:GetRepositoryPolicy&quot;,<br>            &quot;ecr:ListImages&quot;,<br>            &quot;ecs:CreateCluster&quot;,<br>            &quot;ecs:DeregisterContainerInstance&quot;,<br>            &quot;ecs:DiscoverPollEndpoint&quot;,<br>            &quot;ecs:Poll&quot;,<br>            &quot;ecs:RegisterContainerInstance&quot;,<br>            &quot;ecs:StartTask&quot;,<br>            &quot;ecs:StartTelemetrySession&quot;,<br>            &quot;ecs:SubmitContainerStateChange&quot;,<br>            &quot;ecs:SubmitTaskStateChange&quot;<br>          ],<br>          &quot;Resource&quot;: [<br>            &quot;*&quot;<br>          ]<br>        }<br>      ]<br>    }<br>  }<br>},<br>&quot;ECSRole&quot;: {<br>  &quot;Type&quot;: &quot;AWS::IAM::Role&quot;,<br>  &quot;Properties&quot;: {<br>    &quot;ManagedPolicyArns&quot;: [<br>      { &quot;Ref&quot;: &quot;ECSRolePolicy&quot; }<br>    ],<br>    &quot;AssumeRolePolicyDocument&quot;: {<br>      &quot;Version&quot;: &quot;2012-10-17&quot;,<br>      &quot;Statement&quot;: [<br>        {<br>          &quot;Effect&quot;: &quot;Allow&quot;,<br>          &quot;Principal&quot;: {<br>            &quot;Service&quot;: &quot;ec2.amazonaws.com&quot;<br>          },<br>          &quot;Action&quot;: &quot;sts:AssumeRole&quot;<br>        }<br>      ]<br>    }<br>  }<br>},<br>&quot;InstanceProfile&quot; : {<br>  &quot;Type&quot; : &quot;AWS::IAM::InstanceProfile&quot;,<br>  &quot;Properties&quot; : {<br>    &quot;Path&quot; : &quot;/&quot;,<br>    &quot;Roles&quot; : [ { &quot;Ref&quot;: &quot;ECSRole&quot; } ]<br>  }<br>}</pre><p>We can assign it in this way:</p><pre>&quot;Athena&quot;: {<br>  &quot;Type&quot;: &quot;AWS::EC2::Instance&quot;,<br>  &quot;Properties&quot;: {<br>    ...<br>    &quot;IamInstanceProfile&quot; : {&quot;Ref&quot; : &quot;InstanceProfile&quot;},<br>    ...</pre><h4>ECS configuration</h4><p>Still not done. EC2 instance must know the cluster name. Here we can leverage <em>UserData </em>property:</p><pre>&quot;Athena&quot;: {<br>  &quot;Type&quot;: &quot;AWS::EC2::Instance&quot;,<br>  &quot;Properties&quot;: {<br>    ...<br>    &quot;UserData&quot;: { &quot;Fn::Base64&quot;: { &quot;Fn::Join&quot; : [ &quot;\n&quot;, [<br>      &quot;#cloud-config&quot;,<br>      &quot;repo_update: true&quot;,<br>      &quot;repo_upgrade: all&quot;,<br>      &quot;package_upgrade: true&quot;,<br>      &quot;packages:&quot;,<br>      &quot; - aws-cli&quot;,<br>      &quot; - ecs-init&quot;,<br>      &quot;runcmd:&quot;,<br>      { &quot;Fn::Join&quot; : [ &quot;&quot;, [<br>        &quot; - echo ECS_CLUSTER=&quot;,<br>        { &quot;Ref&quot;: &quot;ECSCluster&quot; },<br>        &quot; &gt;&gt; /etc/ecs/ecs.config&quot;<br>      ]]},<br>      &quot; - service docker start&quot;,<br>      &quot; - start ecs&quot; ]]}},<br>      ...</pre><p>And …</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/802/1*kgM3GEX878Po57gFHTWm2Q.png" /></figure><p>… victory ;-) Registered Container Instances shows 1.</p><h3>Service</h3><h4>Task definition</h4><p>I’m not going to cover task definition. You can study it <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_defintions.html">here</a>. It’s pretty straightforward. Kind of stack file for Docker Cloud, but in JSON. Very similar.</p><h4>Travis CI script</h4><p>First part is task registration (or update with the same command). New revision is created if task already exists. It’s a simple sequence 1, 2, 3, …</p><p>Sad is that I didn’t find a way how to specify revision number manually. Just to keep it in sync with $<em>TRAVIS_BUILD_NUMBER</em>.</p><pre><em>aws </em>ecs register-task-definition \<br>  --cli-input-json file://athena-task-definition.json</pre><pre>ARN=<strong>$</strong><em>(aws ecs describe-services --cluster Athena --services athena | jq &#39;.services? | .[0]? | .serviceArn?&#39; -r -M)</em></pre><pre><strong>if [ </strong>&quot;$ARN&quot; <strong>= </strong>&quot;null&quot;<strong> ]</strong>; <strong>then<br>    </strong><em>aws </em>ecs create-service \<br>        --cluster Athena \<br>        --service-name athena \<br>        --task-definition athena \<br>        --deployment-configuration maximumPercent=100,minimumHealthyPercent=0 \<br>        --desired-count 1<br><strong>else<br>    </strong><em>aws </em>ecs update-service \<br>        --cluster Athena \<br>        --service $ARN \<br>        --task-definition athena \<br>        --deployment-configuration maximumPercent=100,minimumHealthyPercent=0 \<br>        --desired-count 1<br><strong>fi</strong></pre><p>Next part is more interesting. We try to get service ARN. If not found, we’re going to create it otherwise we’re going to update it.</p><blockquote>There’s one issue with this script — if you delete service manually, it’s still accessible via AWS CLI (state is inactive). Service ARN is found, but <em>update-service</em> is going to fail; <em>create-service</em> must be used in this case. Better add state check to this script as well.</blockquote><p>Focus on <a href="http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DeploymentConfiguration.html">minimumHealthyPercent</a>. Value <em>0</em> says — feel free to stop my service during deployment. It’s useful in development environment where you can have one tiny instance for example. If you do not set it to <em>0</em>, deployment can fail:</p><ul><li>one tiny instance in cluster,</li><li>not enough memory on your EC2 instance, because old revision is still running,</li><li>port in use, because of the same reason,</li><li>etc.</li></ul><p>Do not use this strategy in production environment. You should have more than one instance, setup <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-load-balancing.html">load balancing</a>, etc.</p><p>That’s it. You have pretty decent idea how EC2 Container Service works. Not all issues were covered, but should be enough to start with it. Hope that helps. It took me some time to find all these things.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3ae12187cf35" width="1" height="1" alt=""><hr><p><a href="https://medium.com/robert-vojta/aws-journey-ec2-container-service-3ae12187cf35">AWS journey — EC2 Container Service</a> was originally published in <a href="https://medium.com/robert-vojta">Zrzka’s adventures</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AWS journey — Cloud Formation]]></title>
            <link>https://medium.com/robert-vojta/aws-journey-cloud-formation-53d0aaaa212a?source=rss-f9e2afc9a25f------2</link>
            <guid isPermaLink="false">https://medium.com/p/53d0aaaa212a</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[cloudformation]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <dc:creator><![CDATA[Robert Vojta]]></dc:creator>
            <pubDate>Fri, 07 Oct 2016 11:40:03 GMT</pubDate>
            <atom:updated>2016-10-07T11:59:35.103Z</atom:updated>
            <content:encoded><![CDATA[<p>As a <a href="https://www.purposefly.com/">small start up</a>, we’re playing with lot of technologies and we try to choose the best ones. Okay, sometimes not because of money constraints, but we’re trying. One day, can’t remember when it was, we decided to go with AWS. Not just EC2 instances for Docker Cloud, but full stack. I mean API Gateway, Lambda, EC2, ECS, DynamoDB, etc. Counted them, AWS provides 51 services. Some of them are perfect, some of them still needs polishing, but they’re pretty good generally speaking.</p><p>Quite huge, isn’t it? Learning curve is pretty steep and you’re going to make lot of mistakes. Sometimes fatal ones. We, especially I, made them. I’m going to share some of them, some tips, so, you can avoid head scratching and long night shifts fixing these mistakes when your customers are asleep.</p><h3>CloudFormation</h3><blockquote>AWS CloudFormation gives developers and systems administrators an easy way to create and manage a collection of related AWS resources, provisioning and updating them in an orderly and predictable fashion.</blockquote><p>Don’t start with CloudFormation if you’re new to the AWS platform. It slows you down, because you have to learn template system, how to use <a href="http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html"><em>Fn::Join</em> &amp; friends</a>, <a href="https://blogs.aws.amazon.com/application-management/post/Tx1T9JYQOS8AB9I/Use-Nested-Stacks-to-Create-Reusable-Templates-and-Support-Role-Specialization">nested stack</a>, <a href="http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html">outputs</a>, <a href="http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html">parameters</a>, … You have to learn it along with other AWS services and it’s not an easy task.</p><p>Don’t take me wrong. I like CloudFormation, I do use it and I recommend it. What I’m trying to say is — don’t try to learn everything at once. One by one. Learn whatever service you choose, then jump into the Cloud Formation and create templates. Why?</p><ul><li>You do not want to learn how to write template when you have no clue how API Gateway &amp; Lambda works for example.</li><li>You do not want to wait for deployment (it’s fast, but not enough sometimes), you want to tweak settings, play with it and test it immediately.</li></ul><p>Also you can easily shoot yourself:</p><ul><li>Resources deployed via CF templates shouldn’t be manually tweaked at all. You can easily update your templates, CF can process these updates and it updates resources automatically. If you touch them manually, CF can be puzzled (state differs) and you can end up in limbo.</li><li>Never delete resources manually. Update template and let CF process it. If you do it in reverse order, CF can be puzzled. You can fix it by recreating resource manually sometimes. Better learn how to work with <a href="http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html">DeletionPolicy</a> attribute.</li><li>What’s more important. Never ever delete nested stack manually. This action is not reversible. You can recreate nested stack with the same name, but it has different PhysicalID and thus the stack is there, but it’s not linked to your parent stack.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/432/1*S7jTsjgWb4uzDbUTKU7I9A.png" /></figure><p>I deleted nested stack manually (frankly, I don’t know why I did it) and I’m angry with myself.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Vk9KiQmEbZWofGkFboq6bw.png" /></figure><p>Hope AWS support guys can help me (tweak resource PhysicalID) and if not I’ll be forced to delete stack, resources, … and create everything “from scratch”.</p><p>“From scratch” in quotes, because we have templates for 99.9999% of our resources. But there are other things to fix with this redeployment like Lambda functions configuration (another beast, will dive into details in another article), Continuous Integration (we do use Travis CI), etc.</p><h3>Nested stacks</h3><p>You can split one stack into several stacks and link them together. You can pass output from one stack as a parameter value to another one, which is really convenient. This is not possible with standalone stacks. I’m tempted to recommend one root stack for all your stacks. You never know when you’ll need output of one stack as an input for another one.</p><p>We have several root stacks, several nested stacks, but these root stacks are not linked together with some kind of top level stack for everything. My hope is recently announced <a href="http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html">Fn::ImportValue</a> function. But there’s one limitation which bothers me little bit:</p><ul><li>You can’t modify or remove the output value as long as it’s referenced by another stack.</li></ul><p>I didn’t find time to test this function. But if I understand it correctly, I can’t change the output value with template update, because the value is kinda “copied” to another stack and then the initial stack is locked. If so, not sure how useful this feature is. Because nested stacks, outputs &amp; parameters, handle this very well. Whenever I change something in one stack, all other dependent stacks are updated accordingly.</p><p>What I’m going to do? Will test <em>Fn::ImportValue</em> and will use it. Or I’ll refactor our stacks and will add 3rd level (top for all stacks).</p><h3>My Cloud Formation workflow</h3><ul><li>I manually create resources, tweak them and make them working together. Especially when I’m trying to learn new AWS service and I don’t know how to do it properly yet.</li><li>If they’re in acceptable state, they work, I do not expect huge changes, work on CF templates is started.</li><li>Then I delete manually created resources, put these templates in our repo where Travis CI handles them automatically (stack creation, updates, …).</li><li>I no longer care about these resources in the AWS Console and I work with templates only.</li></ul><p>Skipping first step when I know what I would like to achieve and <strong>mainly</strong> how to do it.</p><h3>Conclusion</h3><p>Use it, highly recommended, but try to learn other services first. Then learn how to describe them in Cloud Formation templates.</p><p>Think twice:</p><ul><li>Don’t create huge, not easily maintainable stacks. Use nested stacks.</li><li>Is there a tiny chance that you’ll need the output value of one stack as an input value for another stack? Use nested stacks. (or <em>Fn::ImportValue</em>, not tested yet)</li><li>Don’t tweak resources created via CF manually. Don’t delete them as well.</li><li>Learn how <em>DeletionPolicy</em> attribute works.</li><li>Finally, don’t delete nested stack manually = limbo.</li></ul><p>Templates allows me to deploy everything in under 15 minutes without worrying if I did or didn’t make a mistake. Who likes manual and repetitive work? Me not. So, use them.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=53d0aaaa212a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/robert-vojta/aws-journey-cloud-formation-53d0aaaa212a">AWS journey — Cloud Formation</a> was originally published in <a href="https://medium.com/robert-vojta">Zrzka’s adventures</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Yes, I don’t like Slack]]></title>
            <link>https://medium.com/robert-vojta/yes-i-dont-like-slack-bd5e7ae8c5ac?source=rss-f9e2afc9a25f------2</link>
            <guid isPermaLink="false">https://medium.com/p/bd5e7ae8c5ac</guid>
            <category><![CDATA[slack]]></category>
            <category><![CDATA[productivity]]></category>
            <dc:creator><![CDATA[Robert Vojta]]></dc:creator>
            <pubDate>Thu, 16 Jun 2016 20:36:19 GMT</pubDate>
            <atom:updated>2016-06-16T20:36:19.235Z</atom:updated>
            <content:encoded><![CDATA[<p>I don’t know why people are so excited about Slack (or HipChat). I don’t like it and people often ask me why?</p><ul><li>macOS client (web wrapper) performance is terrible, especially in the full screen mode. Same applies to web application as well.</li><li>Search sucks! It’s almost impossible to find what others want from me. Try to quit Slack, do not open it for one day and then try to find mentions of your name, tasks what you should do, etc. Without reading all unread messages — impossible.</li><li>No threads! Remember news groups? Threads, read / unread, … It was a pleasure. It’s not uncommon scenario that you start a thread about specific topic, people step in, chit chat and your topic is lost.</li><li>Both things are forcing me to check Slack channels regularly and react almost immediately (or completely ignore it).</li></ul><p>This behavior has consequences:</p><ul><li>concentration and focus killer,</li><li>you don’t have enough time to thing about the problem (question, proposal, …),</li><li>you’re judging problems too quickly,</li><li>can lead to lot of short “no” answers or no answers at all,</li><li>can lead to frustration of your team members.</li></ul><p>I agree that it’s all about rules and how you (and your team) do use Slack. But hey, people do not follow rules, they have tendency to step into any discussion, kill it, etc. They’re selfish and they don’t care about you.</p><p>My experience says that all #channels are quiet (almost no messages) or they’re full of unreadable and time consuming mess. Or, which is even much more worse, direct messages / private channels — you’re not sharing knowledge, problems, … with others.</p><p>But you know what? I actually like Slack, because all these reply-to-all email chit chats are gone. Guess where they’re now? Yes, in #general.</p><p>Another problem is that written text can’t never replace face to face communication. Emotions are missing and emoji characters can’t simply replace them. Also you can pretend that you’re in a good mood even if you’re upset. Picking smiling emoji character costs nothing.</p><p>Actually we’re experimenting with other technologies like good old Team Speak for example. It has similar issues, like all tools for distributed teams. But it’s much better in terms of emotions — you can hear them in your buddy’s voice and it’s much much quicker when compared to typing. macOS client is ugly, messages sucks, but it’s actually a good thing, because no one writes them :)</p><blockquote>Slack, in it’s current state, is overwhelming and is killing focus. Period.</blockquote><p>P.S. They have a plan to release <a href="http://www.theverge.com/2016/4/13/11418594/slack-threaded-messaging-chat-app-feature">threads</a> later this year. Who knows, maybe I’ll change my opinion, but I doubt it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bd5e7ae8c5ac" width="1" height="1" alt=""><hr><p><a href="https://medium.com/robert-vojta/yes-i-dont-like-slack-bd5e7ae8c5ac">Yes, I don’t like Slack</a> was originally published in <a href="https://medium.com/robert-vojta">Zrzka’s adventures</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AWS Lambda & API Gateway Error Handling]]></title>
            <link>https://medium.com/robert-vojta/aws-lambda-api-gateway-error-handling-ede331178ee2?source=rss-f9e2afc9a25f------2</link>
            <guid isPermaLink="false">https://medium.com/p/ede331178ee2</guid>
            <category><![CDATA[aws-api-gateway]]></category>
            <category><![CDATA[clojurescript]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <dc:creator><![CDATA[Robert Vojta]]></dc:creator>
            <pubDate>Thu, 14 Apr 2016 14:01:09 GMT</pubDate>
            <atom:updated>2016-04-14T14:01:09.608Z</atom:updated>
            <content:encoded><![CDATA[<p>It’s fragile and kind of terrible. Let’s look at it. We have a function named <em>lambda</em>. It has one argument named <em>action</em> (enum with allowed values <em>data</em>, <em>exception</em> or <em>timeout</em>). First thing we would like to do is to validate input via <a href="https://github.com/plumatic/schema">schema</a>. Then we would like to return custom error message (and status code) if it’s invalid or do other things based on the <em>action</em> argument value. Here’s the code, just to demonstrate how it works.</p><pre>(defn- timeout [ms]<br> (let [c (chan)]<br> (js/setTimeout (fn [] (close! c)) ms)<br> c))</pre><pre>(defn- error<br> [code message]<br> (.stringify js/JSON (clj-&gt;js {:error-code code :error-message (pr-str message)})))</pre><pre>(def EventSchema<br> {:action (s/enum “data” “exception” “timeout”)})</pre><pre>(def ^:export lambda<br> (async-lambda-fn<br>  (fn [event context]<br>   (go<br>    (if-let [e (s/check EventSchema event)]<br>     (fail! context (error “INVALID_PARAMS” e))<br>     (try<br>      (let [action (:action event)]<br>       (cond<br>        (= “data” action) (succeed! context {:some “data”})<br>        (= “timeout” action) (&lt;! (timeout 5000))<br>        (= “exception” action) (throw (js/Error. “Oops”))))<br>     (catch :default e<br>      (fail! context (error “EXCEPTION” e)))))))))</pre><p>I’m not going to describe how to deploy Lambda function or API Gateway basics. I assume you know how to do it. I’m going to focus on error handling only.</p><p>Our function is deployed, API Gateway resource (<em>/lambda</em>) and <em>GET</em> method created and deployed as well. All with default settings, just for now.</p><p>What happens when we try to use it?</p><pre>curl -sw “\n%{http_code}” -H “x-api-key: ...” -H “Accept: application/json” https://...execute-api.eu-west-1.amazonaws.com/latest/lambda</pre><pre>{“errorMessage”:”{\”error-code\”:\”INVALID_PARAMS\”,\”error-message\”:\”{:action missing-required-key}\”}”}<br>200</pre><p>JSON in JSON and status code is 200. <strong>That’s not what we want. At all.</strong></p><h4>Pass action from query to lambda function</h4><p><em>POST</em> method handles this automatically. Body is passed via <em>event</em> argument. But <em>GET</em> is a slightly different beast.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kfT1PBJX-IPoMH3sIrAzsA.png" /><figcaption>action in URL Query String Parameters</figcaption></figure><p>We have to add <em>action</em> to the URL Query String Parameters section. Then we have to add Body Mapping Template (BMT) in the Integration Request section.</p><pre>{&quot;action&quot; : &quot;$input.params(&#39;action&#39;)&quot;}</pre><p>Redeploy API and test.</p><pre>{“errorMessage”:”{\”error-code\”:\”INVALID_PARAMS\”,\”error-message\”:\”{:action (not (#{\\\”data\\\” \\\”exception\\\” \\\”timeout\\\”} \\\”\\\”))}\”}”}<br>200</pre><p>Much better, we can see validation response. Let’s try some [in]valid options. Status code 200 for all of them with following responses.</p><pre><strong>action=data</strong><br>{“some”:”data”}</pre><pre><strong>action=timeout</strong><br>{“errorMessage”:”2016–04–14T12:20:20.584Z 3f0e0ca9–023b-11e6-a561-e58695fc8058 Task timed out after 4.00 seconds”}</pre><pre><strong>action=exception</strong><br>{“errorMessage”:”{\”error-code\”:\”EXCEPTION\”,\”error-message\”:\”#object[Error Error: Oops]\”}”}</pre><p>NOTE: Our lambda function is deployed with <em>timeout</em> set to 4 seconds. Just to make it working, check the code, we’re waiting 5 seconds.</p><p>This is not good. We want 200 for success, 400 for invalid input parameters and 500 if something bad happens (exception, timeout, …) and better structure of our response.</p><h4>Mapping status codes</h4><p>We have to add 400 and 500 in the Method Response section. Otherwise we’ll not be able to select these status codes in the Integration Response section.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KSbnbenQk15ou9-5k21xQg.png" /><figcaption>New status codes 400, 500</figcaption></figure><p>If lambda function fails, it returns JSON in the following format:</p><pre>{“errorMessage” : ”whatever”}</pre><p>And the <em>errorMessage</em> is a string. Do you want to return more than just a string? You have to encode it.</p><p>Open the Integration Response section and you’ll see something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SJeuXFb1csNqURXxp5Dj4w.png" /><figcaption>Default response</figcaption></figure><p>Default mapping uses 200 for the status code, there’s no regex and Body Mapping Template (click on the triangle) is empty = passthrough. And because we know what our Lambda function returns, we can add some regular expressions there.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YdAvuEBB-GtI3MI4XAx6qA.png" /><figcaption>Response mapping</figcaption></figure><p>What’s this all about? Lambda Error Regex column contains regular expressions that are matched against <em>errorMessage</em> from the lambda response if it indicates it failed (<em>fail!</em> in the code). For <em>INVALID_PARAMS</em> we would like to return 400, for any other error message (<em>.+</em>) we would like to return 500 and 200 for the rest.</p><p>Redeploy and test.</p><pre><strong>action=exception</strong><br>{“errorMessage”:”{\”error-code\”:\”EXCEPTION\”,\”error-message\”:\”#object[Error Error: Oops]\”}”}<br>500</pre><pre><strong>action=timeout</strong><br>{“errorMessage”:”2016–04–14T12:41:09.348Z 27638414–023e-11e6-bcf3–89b1797fdc7b Task timed out after 4.00 seconds”}<br>500</pre><pre><strong>action=hallo</strong><br>{“errorMessage”:”{\”error-code\”:\”INVALID_PARAMS\”,\”error-message\”:\”{:action (not (#{\\\”data\\\” \\\”exception\\\” \\\”timeout\\\”} \\\”hallo\\\”))}\”}”}<br>400</pre><pre><strong>action=data</strong><br>{“some”:”data”}<br>200</pre><h4>Better response structure</h4><p>Status codes solved. Now we have to focus on the response structure. It’s quite simple. We can do it in the Integration Response section as well. We have to add Body Mapping Template for each regular expression we already added.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TN83Hnr9axF-myITEvJrfg.png" /><figcaption>Body Mapping Template for 400</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ctZcjZTjOCAnxb2SRO6W0A.png" /><figcaption>Body Mapping Template for 500</figcaption></figure><p>As you can see, BMT for 500 is more fancier that the one for 400. Why? AWS can kill our lambda function because of timeout for example and then there’s just plain string in the <em>errorMessage</em>. Also our lambda can catch exception, send back custom error map and then there’s encoded JSON in the <em>errorMessage</em>. And because we <strong>can’t add</strong> two (or more) regular expressions for the same status (500 in this case), we have to handle it in the BMT.</p><p>Redeploy and test.</p><pre><strong>action=exception</strong><br>{“error-code” : “EXCEPTION”,<br> “error-message” : “#object[Error Error: Oops]”}<br>500</pre><pre><strong>action=timeout<br></strong>{“error-code” : “AWS”,<br>  “error-message” : “2016–04–14T13:50:09.212Z caf1daed-0247–11e6–91f8-d9f9a69fef2c Task timed out after 4.00 seconds”}<br>500</pre><pre><strong>action=hallo</strong><br>{“error-code” : “INVALID_PARAMS”,<br> “error-message” : “{:action (not (#{“data” “exception” “timeout”} “hallo”))}”}<br>400</pre><pre><strong>action=date</strong><br>{“some”:”data”}<br>200</pre><p>Goal reached. Status codes and better response structure for errors. It’s not perfect, there’s always room for improvement, but it works.</p><p>It’s a bare minimum you have to do. Pretty rough, okay, terrible, but when you know how to do it … Hope it helps, because I spent quite huge amount of time on this.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ede331178ee2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/robert-vojta/aws-lambda-api-gateway-error-handling-ede331178ee2">AWS Lambda &amp; API Gateway Error Handling</a> was originally published in <a href="https://medium.com/robert-vojta">Zrzka’s adventures</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Giving Medium a Try]]></title>
            <link>https://medium.com/robert-vojta/giving-medium-a-try-baf6e2a98c1c?source=rss-f9e2afc9a25f------2</link>
            <guid isPermaLink="false">https://medium.com/p/baf6e2a98c1c</guid>
            <category><![CDATA[github]]></category>
            <category><![CDATA[git]]></category>
            <dc:creator><![CDATA[Robert Vojta]]></dc:creator>
            <pubDate>Mon, 11 Apr 2016 05:42:51 GMT</pubDate>
            <atom:updated>2016-04-11T05:42:51.141Z</atom:updated>
            <content:encoded><![CDATA[<p>We started <a href="https://medium.com/purpose-fly">PurposeFly blog</a> on Medium recently and I decided to give it a try for my personal blog as well. Old posts are still available at <a href="http://old.robertvojta.com/">old.robertvojta.com</a>. Err, will be available when I fix <em>relative_permalinks</em> issue and DNS records.</p><p>Not that I don’t like Jekyll and GitHub Pages. But the workflow is kinda crazy when I want to publish new post and the only device I have is my iPad.</p><p>I’ll give it a try for a while and then I’ll merge them. Either on GitHub Pages or here. We’ll see.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=baf6e2a98c1c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/robert-vojta/giving-medium-a-try-baf6e2a98c1c">Giving Medium a Try</a> was originally published in <a href="https://medium.com/robert-vojta">Zrzka’s adventures</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>