This repository has been archived by the owner on May 12, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.html
1134 lines (863 loc) · 245 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Google Analytics -->
<script type="text/javascript">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-130626812-2', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->
<title>Cloud Republic Blog</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="keywords" content="Azure, Development, Office365, DevOps, Cloud, Front-end">
<meta property="og:type" content="website">
<meta property="og:title" content="Cloud Republic Blog">
<meta property="og:url" content="https://cloudrepublic.github.io/index.html">
<meta property="og:site_name" content="Cloud Republic Blog">
<meta property="og:locale" content="EN">
<meta name="twitter:card" content="summary">
<link rel="alternate" href="/atom.xml" title="Cloud Republic Blog" type="application/atom+xml">
<link rel="icon" href="/favicon.png">
<link href="//fonts.googleapis.com/css?family=Source+Code+Pro" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div id="container">
<div id="wrap">
<header id="header">
<div id="banner"></div>
<div id="header-outer" class="outer">
<div id="header-title" class="inner">
<h1 id="logo-wrap">
<a href="/" id="logo">Cloud Republic Blog</a>
</h1>
<h2 id="subtitle-wrap">
<a href="/" id="subtitle">De Cloud Developers van morgen.</a>
</h2>
</div>
<div id="header-inner" class="inner">
<nav id="main-nav">
<a id="main-nav-toggle" class="nav-icon"></a>
<a class="main-nav-link" href="/">Home</a>
<a class="main-nav-link" href="/archives">Archives</a>
</nav>
<nav id="sub-nav">
<a id="nav-rss-link" class="nav-icon" href="/atom.xml" title="RSS Feed"></a>
<a id="nav-search-btn" class="nav-icon" title="Search"></a>
</nav>
<div id="search-form-wrap">
<form action="//google.com/search" method="get" accept-charset="UTF-8" class="search-form"><input type="search" name="q" class="search-form-input" placeholder="Search"><button type="submit" class="search-form-submit"></button><input type="hidden" name="sitesearch" value="https://cloudrepublic.github.io"></form>
</div>
</div>
</div>
</header>
<div class="outer">
<section id="main">
<article id="post-Unit-Testing-improvements" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/03/15/Unit-Testing-improvements/" class="article-date">
<time datetime="2020-03-15T08:23:54.000Z" itemprop="datePublished">2020-03-15</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/03/15/Unit-Testing-improvements/">Unit Testing improvements</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>Tests are stories we tell the next generation of programmers on a project. If you are to lazy to write them well consider moving to a different profession. This isn’t a job for you.</p>
<p>Source: <a href="https://www.manning.com/books/the-art-of-unit-testing-second-edition" target="_blank" rel="noopener">Roy Osherove - The Art of Unit Testing</a></p>
<p>In a world where we use CI / CD pipelines to automate our releases unit tests are an essential part of a successful software project. Not only do they help us to debug the code we are writing faster but they also help us to validate the code before releasing it to our users.</p>
<p>In this post, we will go over the steps you can take to improve your unit tests and therefore create a better product in the end.</p>
<h1 id="Naming"><a href="#Naming" class="headerlink" title="Naming"></a>Naming</h1><p>Using a good naming convention for a unit test does not necessarily help you to write a better product but it will help your team to identify clearly what unit is tested and what state the unit under test is in.</p>
<p>One of the most know conventions that are currently used is <code>[MethodUnderTest]_[Scenario]_[ExpectedResult]</code>. While it is not a bad naming convention it is limited. Writing the naming of the unit test this way leaves out some imported details and might not make it clear to other programmers what the test is trying to test. With naming it is better to tell the story of the test. Let’s look at two examples.</p>
<p>In this example, we are looking at a static method that checks on an order if the delivery is valid. Using the convention mentioned above we would get something like this:</p>
<p><code>IsDeliveryValid_InvalidDate_ReturnsFalse</code></p>
<p>While this is giving us a lot of information about what the unit under test is supposed to do it does not describe the scenario that we want to validate. Let’s look at a better example:</p>
<p><code>Delivery_with_invalid_date_should_be_considered_invalid</code></p>
<p>The above example describes the behavior of the application we are trying to test rather than the code that will be part of the test. This also will help new programmers to understand the application better with limited domain knowledge.</p>
<h1 id="Test-method-layout"><a href="#Test-method-layout" class="headerlink" title="Test method layout"></a>Test method layout</h1><p>Another way to make your tests more readable and separate the parts of your tests is by using the <code>Arrange</code>, <code>Act</code>, and <code>Assert</code> pattern or <code>AAA</code> for short. using this pattern we split our test into 3 distinct parts.</p>
<p>The <code>Arrange</code> section is used to initialize objects and gather data needed for the test or set local variables.</p>
<p>The <code>Act</code> section is used to invoke the method that this test is designed for.</p>
<p>The <code>Assert</code> section is used to verify that we received the expected result from invoking the method under test.</p>
<p>Using this pattern a test could look like this:</p>
<p><img src="/images/unit-testing-improvements/code.png" alt="/images/unit-testing-improvements/code.png"></p>
<h1 id="Simplifying-tests-and-reusing-code"><a href="#Simplifying-tests-and-reusing-code" class="headerlink" title="Simplifying tests and reusing code"></a>Simplifying tests and reusing code</h1><p>If you are writing tests you often have to instantiate objects that are under test and bring them into a specific state to perform the test. Instantiating all these objects in the test method can clutter up your test and lead to a lot of duplicate code.</p>
<p>The builder pattern is here to help. With this pattern, we can bring the objects into a state step by step to meet the requirements of our test. To use the builder pattern we create extension methods that allow us to buildup the object to the desired state. This helps to keep our tests clear, more readable and results in less duplicated code during testing.</p>
<p>In this example, we will be testing the method that checks if an order is ready to be shipped. Since the order is an object that is heavily used in the system we would not want to create it over and over again. Let’s have a look at the order object:</p>
<p><img src="/images/unit-testing-improvements/code%201.png" alt="/images/unit-testing-improvements/code%201.png"></p>
<p>As you can see we have a method called <code>CanShip</code>. We should test this method to ensure that for example, we do not ship orders to customers that are not paid yet. We might also want to test if <code>CanShip</code> still returns false if the Address is not set yet. We already have two tests to write where we need a new instance of the Order class in a specific state. Creating a new instance of the Order class every time from scratch would not be efficient.</p>
<p>So let’s start making use of the builder pattern to buildup the Order class. First, we need a base state that we call <code>Default</code> and we create an extension method for it:</p>
<p><img src="/images/unit-testing-improvements/code%202.png" alt="/images/unit-testing-improvements/code%202.png"></p>
<p>With this extension in place, we can start and write our first unit test for the Order class. The unit test will look as follows:</p>
<p><img src="/images/unit-testing-improvements/code%203.png" alt="/images/unit-testing-improvements/code%203.png"></p>
<p>As you can see from the picture above we used the extension method to get the order in the default state. What this default state means for you or your team can depend on the object in the test or on how the object is used in the system. In our example default just sets all the fields to a valid value and sets the status to new.</p>
<p>From hereon, we can keep using this method in other tests as well. Now let’s imagine we add order-lines to our order class like this:</p>
<p><img src="/images/unit-testing-improvements/code%204.png" alt="/images/unit-testing-improvements/code%204.png"></p>
<p>We can now create a new extension method for our order that allows us to build a default order with order-lines. The extension method would look like this:</p>
<p><img src="/images/unit-testing-improvements/code%205.png" alt="/images/unit-testing-improvements/code%205.png"></p>
<p>In the method above I’ve created the order-line from scratch but let’s say we want to test this object in multiple tests we could create the same <code>Default</code> extension method as we did for the order class. We can now use this extension method in our next test like this:</p>
<p><img src="/images/unit-testing-improvements/code%206.png" alt="/images/unit-testing-improvements/code%206.png"></p>
<p>As you can see we can make the test a lot smaller and with less repetitive code. It is like ordering a pizza and saying which toppings you want on the pizza.</p>
<h1 id="Mocking"><a href="#Mocking" class="headerlink" title="Mocking"></a>Mocking</h1><p>With mocking we emulate behavior during our tests that are outside of the scope that we need to test but is needed for the test to run. A good example would be a method the does some validation logic and then gets an order from the database. We would want to test the validation logic but not the database call because this would be more like a regression test.</p>
<p>In the .Net world, there are a lot of Nuget packages that can help you with mocking but one of the more popular ones is <code>Moq</code>. This is also the package we are gonna use. While writing the mocking of for example the database call we can use some of the things we learned earlier. In the example below, we are gonna use the builder pattern again to get rid of the mocking code and move it to an extension method.</p>
<p>Lets take the example we described above. The method could look like this:</p>
<p><img src="/images/unit-testing-improvements/code%207.png" alt="/images/unit-testing-improvements/code%207.png"></p>
<p>We are checking that the <code>orderId</code> is not null or empty and then we are calling the <code>GetOrder</code> method to get the Order from the database.</p>
<p>If we want to mock this method we need to set up a mock instance of the database class. The setup for this mock will look like this:</p>
<p><img src="/images/unit-testing-improvements/code%208.png" alt="/images/unit-testing-improvements/code%208.png"></p>
<p>As you can see we are using the Default extension here on the Order class to create a new default Order. Now we could set up the database every time like this in every test we use but that would be tedious to write the same code every time so let’s create another extension method:</p>
<p><img src="/images/unit-testing-improvements/code%209.png" alt="/images/unit-testing-improvements/code%209.png"></p>
<p>With this extension method, we can quickly set up a new database and return a default order whenever we need to during testing. So let’s see what a test would look like using this extension method:</p>
<p><img src="/images/unit-testing-improvements/code%2010.png" alt="/images/unit-testing-improvements/code%2010.png"></p>
<p>In the beginning, it seems like a lot of extra work to create all these extension methods but ones you have a lot of tests that reuse the extension methods they feel like a big time saver.</p>
<h1 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h1><p>As we saw in the scenarios described above we can make some nice improvements to our unit tests to slim them down and make the code more reusable. I would not recommend to rewrite all you tests straight away but ones you need to fix a test or write new ones you can use these tips to make them better.</p>
</div>
<footer class="article-footer">
<a data-url="https://cloudrepublic.github.io/2020/03/15/Unit-Testing-improvements/" data-id="ckasa0xhi0001awun32jeb2b4" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Clean-Code/" rel="tag">Clean Code</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Mocking/" rel="tag">Mocking</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Unit-Test-Naming/" rel="tag">Unit Test Naming</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Unit-Testing/" rel="tag">Unit Testing</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Unit-Testing-improvements/" rel="tag">Unit Testing improvements</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/xUnit/" rel="tag">xUnit</a></li></ul>
</footer>
</div>
</article>
<article id="post-Azure-Blueprints" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/03/04/Azure-Blueprints/" class="article-date">
<time datetime="2020-03-04T21:03:39.000Z" itemprop="datePublished">2020-03-04</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/03/04/Azure-Blueprints/">Azure-Blueprints</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>When we think about a blueprint, we usually think about a technical drawing or guide for making something. It’s a design or pattern that can be followed. If we, for example, want to create a treehouse, then we would draw a blueprint and follow the design carefully to always have the same outcome as the blueprint instructs. Azure Blueprints works in much the same way. IT Engineers can use an Azure Blueprint to design and deploy Azure resources to Azure according to different rules and patterns that an enterprise has. By using these blueprints it becomes really easy to quickly deploy resources that adhere to the rules<br>and requirements of an enterprise.</p>
<h2 id="Case-Description"><a href="#Case-Description" class="headerlink" title="Case Description"></a>Case Description</h2><img src="/images/azure-blueprints/CAF-Governance.png" />
<p>When we look at an enterprise, we usually see that they lack the tools to enforce the governance of Azure Resources. The way they enforce rules on resources is done by a group within the company that is responsible for teaching the development teams how to use Azure they way that the company wants it. This teaching can be done in many different ways, one of which could be a document describing how to create resources and what rules apply when creating them. The development teams can then start using these documents to create the resources they need.</p>
<p>One of the problems that might occur is that the development team makes a mistake while trying to set up the resources they need. When this happens they will go to the central group that is responsible for Azure and ask them how to fix this. This essentially creates a bottleneck within the company. You can manage when one or two teams ask questions or need assistance when creating resources. But when you have more than ten teams the central team will be delayed in their work. </p>
<p>Also what can happen is that the enterprise can enforce governance when things are already deployed to Azure. This could cause some problems as other resources might have to be reconfigured to adhere to the new rules.</p>
<p>This is where Azure Blueprints comes into play! By using Azure Blueprints not only can we define how the resources should be made and used so that we always end up with the same result as the Blueprint describes. We can also check upon these created resources and make sure that they comply to the set of rules made by the enterprise. This makes sure that the enterprise does not have to interfere everytime resources are requested as they have already made sure that the blueprints use their rules.</p>
<p>This removes the bottleneck described above as development teams can ask the group responsible for the governance of Azure Resources to create certain resources for them. All the group has to do is use the correct blueprint on the subscription of the development team and they will have the resources they request without having to do anything themselves.</p>
<h2 id="What-is-Azure-Blueprints"><a href="#What-is-Azure-Blueprints" class="headerlink" title="What is Azure Blueprints?"></a>What is Azure Blueprints?</h2><p>The goal of Azure Blueprints is to assist development teams or central Azure Architecture teams in deploying new environments easily and quickly but still adhere to the companies rules. </p>
<p>There are a few artifacts that are used within Azure Blueprints: </p>
<ul>
<li>Role Assignments </li>
<li>Policy Assignments </li>
<li>ResourceGroups </li>
<li>ARM Templates </li>
</ul>
<p>A blueprint is essentially a package that uses all these types of resources and artifacts together. this package then contains resources that comply with organizational standards and best practices. </p>
<img src="/images/azure-blueprints/blueprint-diagram.png" />
<h2 id="Dive-into-Blueprints"><a href="#Dive-into-Blueprints" class="headerlink" title="Dive into Blueprints"></a>Dive into Blueprints</h2><p>Like most resources in Azure, a blueprint in Azure Blueprints has a natural lifecycle. A blueprint can be created and then deployed and when they are no longer needed they can be removed. </p>
<p>Azure blueprints also provides support for continuous integration and deployment. </p>
<p>An Azure Blueprint lifecycle typically consists of: </p>
<ul>
<li>Creating a blueprint </li>
<li>Publishing a blueprint </li>
<li>Creating and editing a new version of the blueprint </li>
<li>Publishing a new version of a blueprint </li>
<li>Deleting a specific version of a blueprint </li>
<li>Deleting a blueprint </li>
</ul>
<p>After filling in the meta data for the blueprint it is time to create the actual blueprint. </p>
<h2 id="Creating-Blueprint"><a href="#Creating-Blueprint" class="headerlink" title="Creating Blueprint "></a>Creating Blueprint </h2><p>We will be creating a new blueprint in the Azure portal. There are currently options to do this with either Azure CLI or the SDK that is available for Azure Blueprints. </p>
<p>To find Azure Blueprints on Azure either look under the Policy service or go to all resouces and search for blueprint </p>
<p>After clicking on the blueprint service you should end up in the Azure Blueprint Blade. On this blade click on the <strong>Create</strong> button </p>
<img src="/images/azure-blueprints/create-blueprint.png" />
<p>Before we can add artifacts to a blueprint, we first need to give this blueprint a name and a location where we will save the blueprint.</p>
<img src="/images/azure-blueprints/blueprint-metadata.png" />
<p>We have a few options when picking a location. We can pick between a Management Group or a single subscription.<br>More information on management groups can be found here: <a href="https://docs.microsoft.com/nl-nl/azure/governance/management-groups/overview" target="_blank" rel="noopener">https://docs.microsoft.com/nl-nl/azure/governance/management-groups/overview</a></p>
<h2 id="Filling-the-artifact-with-content"><a href="#Filling-the-artifact-with-content" class="headerlink" title="Filling the artifact with content"></a>Filling the artifact with content</h2><p>Once all the data is filled in correctly, click on <strong><em>Next: Artifacts</em></strong></p>
<p>Now we can create the content of the blueprint itself. Here we can add things like Role assignements, ARM templates and Policies.<br><img src="/images/azure-blueprints/add-artifact.png" /></p>
<p>For this example I made a blueprint that will create a resource group with a storage account. The storage account is being created by an arm template and I made sure that the users that have<br>access to this resource group have the contributor role. The allowed Locations policy that is added restricts users from adding locations that are not allowed.</p>
<img src="/images/azure-blueprints/blueprint-artifacts.png" />
<p>Now that we have given this Blueprint some content we can click on Save Draft to save the blueprint. The blueprint will now be saved but won’t be deployed just yet.</p>
<h2 id="Publishing-the-blueprint"><a href="#Publishing-the-blueprint" class="headerlink" title="Publishing the blueprint"></a>Publishing the blueprint</h2><p>The blueprint that we just created will now be saved as a draft (we have now “drawn” the blueprint, but haven’t created anything with it yet). Let’s start publishing our blueprint and assigning it to a subscription.</p>
<p>Before we can assign a blueprint and start deploying it to a subscription we need to publish it. By clicking on the <strong><em>Publish Blueprint</em></strong> button the blueprint will be taken out of the draft status. This means that it is not possible to make any changes to this version of the blueprint anymore.</p>
<img src="/images/azure-blueprints/publish-blueprint.png" />
<p>When publishing the blueprint it is required to give it a <strong><em>version number</em></strong>. When editing a published version we essential create another draft with a different version number. On assignment level we can then select the version number that we want to use.<br>After publishing the blueprint, The edit button turns into a <strong><em>Assign Blueprint button</em></strong>. Click on this to start assigning this blueprint.</p>
<p>On the assign Blueprint blade we can select a subscription that we want to deploy this blueprint to, some metadata about the blueprint itself. One important this we can do is select what version we want to deploy to the subscription. This gives us the flexability to deploy different versions to different subscriptions depending on the requirements.</p>
<img src="/images/azure-blueprints/assign-blueprint.png" />
<p>After filling in the basic data we leave the Lock setting to <strong><em>Don’t Lock</em></strong><br>Then we want to fill in the parameter values for the artifacts. Some of the values need to be filled in, others are already filled in if this data was given when creating the artifact.<br>Lastly it is time to click on Assign to assign the blueprint and start the deployment process.</p>
<p>After the blueprint is assigned and the deployment process is completed we could now see that the storage account showed up in the resourcegroup that we created with this blueprint.</p>
<h1 id="Blueprints-as-code"><a href="#Blueprints-as-code" class="headerlink" title="Blueprints as code"></a>Blueprints as code</h1><p>Ofcourse it is also possible to create an Azure Blueprint without using the portal. This gives you alot of freedom as you can create blueprints during a build or release pipeline or create your own code that can manage the these blueprints.</p>
<p>For this example I will be using PowerShell to create a blueprint and publish it.</p>
<p>Before we can start creating blueprints with PowerShell we first need to make sure that the <a href="https://docs.microsoft.com/nl-nl/azure/governance/blueprints/how-to/manage-assignments-ps#add-the-azblueprint-module" target="_blank" rel="noopener">Az.Blueprint module</a> is installed.</p>
<h2 id="Create-a-blueprint"><a href="#Create-a-blueprint" class="headerlink" title="Create a blueprint"></a>Create a blueprint</h2><p>Azure Blueprints makes it possible to create a repeatable set of Azure Resources that adhere to the rules and requirements of an organization. It can be used to keep track of what is deployed. What version is deployed where and makes it really easy to update a rule if required. Azure Blueprints also comes with an SDK and API, which gives you the chance to automate this process.</p>
<p>To create a blueprint we need to first start out by composing a blueprint. This can be done by creating a json file with different resources. We’ll start by creating a blueprint named ‘CloudRepublicBlueprint’ so that we can configure role and policy assignments for a subscription. Next we will add a resourcegroup and storage account to the blueprint. Finally we will publish the blueprint with a version number so that we can assign it to a subscription.</p>
<p>Start out by creating a blueprint.json file:</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"> {</span><br><span class="line"> <span class="attr">"properties"</span>: {</span><br><span class="line"> <span class="attr">"description"</span>: <span class="string">"This blueprint sets tag policy and role assignment on the subscription, creates a ResourceGroup, and deploys a resource template and role assignment to that ResourceGroup."</span>,</span><br><span class="line"> <span class="attr">"targetScope"</span>: <span class="string">"subscription"</span>,</span><br><span class="line"> <span class="attr">"parameters"</span>: {</span><br><span class="line"> <span class="attr">"storageAccountType"</span>: {</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"string"</span>,</span><br><span class="line"> <span class="attr">"defaultValue"</span>: <span class="string">"Standard_LRS"</span>,</span><br><span class="line"> <span class="attr">"allowedValues"</span>: [</span><br><span class="line"> <span class="string">"Standard_LRS"</span>,</span><br><span class="line"> <span class="string">"Standard_GRS"</span>,</span><br><span class="line"> <span class="string">"Standard_ZRS"</span>,</span><br><span class="line"> <span class="string">"Premium_LRS"</span></span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">"metadata"</span>: {</span><br><span class="line"> <span class="attr">"displayName"</span>: <span class="string">"storage account type."</span>,</span><br><span class="line"> <span class="attr">"description"</span>: <span class="literal">null</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"tagName"</span>: {</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"string"</span>,</span><br><span class="line"> <span class="attr">"metadata"</span>: {</span><br><span class="line"> <span class="attr">"displayName"</span>: <span class="string">"The name of the tag to provide the policy assignment."</span>,</span><br><span class="line"> <span class="attr">"description"</span>: <span class="literal">null</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"tagValue"</span>: {</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"string"</span>,</span><br><span class="line"> <span class="attr">"metadata"</span>: {</span><br><span class="line"> <span class="attr">"displayName"</span>: <span class="string">"The value of the tag to provide the policy assignment."</span>,</span><br><span class="line"> <span class="attr">"description"</span>: <span class="literal">null</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"contributors"</span>: {</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"array"</span>,</span><br><span class="line"> <span class="attr">"metadata"</span>: {</span><br><span class="line"> <span class="attr">"description"</span>: <span class="string">"List of AAD object IDs that is assigned Contributor role at the subscription"</span>,</span><br><span class="line"> <span class="attr">"strongType"</span>: <span class="string">"PrincipalId"</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"owners"</span>: {</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"array"</span>,</span><br><span class="line"> <span class="attr">"metadata"</span>: {</span><br><span class="line"> <span class="attr">"description"</span>: <span class="string">"List of AAD object IDs that is assigned Owner role at the resource group"</span>,</span><br><span class="line"> <span class="attr">"strongType"</span>: <span class="string">"PrincipalId"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"resourceGroups"</span>: {</span><br><span class="line"> <span class="attr">"storageRG"</span>: {</span><br><span class="line"> <span class="attr">"description"</span>: <span class="string">"Contains the resource template deployment and a role assignment."</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<hr>
<p>this blueprint.json file will be used to create the blueprint. Run the following script with the <code>blueprint.json</code> to create a draft of this blueprint:</p>
<figure class="highlight ps"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$blueprint</span> = New-AzBlueprint -Name <span class="string">'CloudRepublicBlueprint'</span> -BlueprintFile .\blueprint.json</span><br></pre></td></tr></table></figure>
<p>This will create the blueprint in the subscription that is selected by default. To specify a subscription use <code>SubscriptionId</code>.</p>
<h3 id="Assigning-Resource-Template"><a href="#Assigning-Resource-Template" class="headerlink" title="Assigning Resource Template"></a>Assigning Resource Template</h3><p>now that the storage account is created, we can add policies and Azure Resource Template to this resource group.</p>
<p>The following code will add a storage account where we can add the parameters when we assign the blueprint to a subscription:</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"$schema"</span>: <span class="string">"https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#"</span>,</span><br><span class="line"> <span class="attr">"contentVersion"</span>: <span class="string">"1.0.0.0"</span>,</span><br><span class="line"> <span class="attr">"parameters"</span>: {</span><br><span class="line"> <span class="attr">"CloudRepublicStorageAccountType"</span>: {</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"string"</span>,</span><br><span class="line"> <span class="attr">"metadata"</span>: {</span><br><span class="line"> <span class="attr">"description"</span>: <span class="string">"Storage Account type"</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"CloudRepublicTagName"</span>: {</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"string"</span>,</span><br><span class="line"> <span class="attr">"defaultValue"</span>: <span class="string">"NotSet"</span>,</span><br><span class="line"> <span class="attr">"metadata"</span>: {</span><br><span class="line"> <span class="attr">"description"</span>: <span class="string">"Tag name from blueprint"</span></span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"tagValue"</span>: {</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"string"</span>,</span><br><span class="line"> <span class="attr">"defaultValue"</span>: <span class="string">"NotSet"</span>,</span><br><span class="line"> <span class="attr">"metadata"</span>: {</span><br><span class="line"> <span class="attr">"description"</span>: <span class="string">"Tag value from blueprint"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"variables"</span>: {</span><br><span class="line"> <span class="attr">"storageAccountName"</span>: <span class="string">"[concat(uniquestring(resourceGroup().id), 'standardsa')]"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"resources"</span>: [{</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"Microsoft.Storage/storageAccounts"</span>,</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"[variables('storageAccountName')]"</span>,</span><br><span class="line"> <span class="attr">"apiVersion"</span>: <span class="string">"2016-01-01"</span>,</span><br><span class="line"> <span class="attr">"tags"</span>: {</span><br><span class="line"> <span class="attr">"[parameters('tagName')]"</span>: <span class="string">"[parameters('tagValue')]"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"location"</span>: <span class="string">"[resourceGroup().location]"</span>,</span><br><span class="line"> <span class="attr">"sku"</span>: {</span><br><span class="line"> <span class="attr">"name"</span>: <span class="string">"[parameters('storageAccountType')]"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"Storage"</span>,</span><br><span class="line"> <span class="attr">"properties"</span>: {}</span><br><span class="line"> }],</span><br><span class="line"> <span class="attr">"outputs"</span>: {</span><br><span class="line"> <span class="attr">"storageAccountSku"</span>: {</span><br><span class="line"> <span class="attr">"type"</span>: <span class="string">"string"</span>,</span><br><span class="line"> <span class="attr">"value"</span>: <span class="string">"[variables('storageAccountName')]"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>Following code will use the parameters <code>CloudRepublicStorageAccountType</code> , <code>CloudRepublicTagName</code> and <code>CloudRepublicTagValue</code> that we need to provide in a parameter file</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"$schema"</span>: <span class="string">"https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#"</span>,</span><br><span class="line"> <span class="attr">"contentVersion"</span>: <span class="string">"1.0.0.0"</span>,</span><br><span class="line"> <span class="attr">"parameters"</span>: {</span><br><span class="line"> <span class="attr">"CloudRepublicStorageAccountType"</span>: {</span><br><span class="line"> <span class="attr">"value"</span>: <span class="string">"[parameters('storageAccountType')]"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"CloudRepublicTagName"</span>: {</span><br><span class="line"> <span class="attr">"value"</span>: <span class="string">"[parameters('tagName')]"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"CloudRepublicTagValue"</span>: {</span><br><span class="line"> <span class="attr">"value"</span>: <span class="string">"[parameters('tagValue')]"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>To assign the ARM template to the draft blueprint we run the following PowerShell script:</p>
<figure class="highlight ps"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">New-AzBlueprintArtifact -Blueprint <span class="variable">$blueprint</span> -Type TemplateArtifact -Name <span class="string">'CloudRepublicStorage'</span> -TemplateFile .\storageTemplate.json -TemplateParameterFile .\storageTemplateParameters.json -ResourceGroupName storageRG</span><br></pre></td></tr></table></figure>
<h3 id="Adding-policies-or-role-assignments"><a href="#Adding-policies-or-role-assignments" class="headerlink" title="Adding policies or role assignments"></a>Adding policies or role assignments</h3><p>To add a policy or role assignment we need to create a json object for this as well. The example below uses the definition identifier for the <strong>Owner</strong> role which is the build in GUID <code>8e3af657-a8ff-443c-a75c-2fe8c4bcb635</code> </p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"roleAssignment"</span>,</span><br><span class="line"> <span class="attr">"properties"</span>: {</span><br><span class="line"> <span class="attr">"resourceGroup"</span>: <span class="string">"storageRG"</span>,</span><br><span class="line"> <span class="attr">"roleDefinitionId"</span>: <span class="string">"/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635"</span>,</span><br><span class="line"> <span class="attr">"principalIds"</span>: <span class="string">"[parameters('owners')]"</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>PowerShell command to add the role assignment to the blueprint</p>
<pre><code>New-AzBlueprintArtifact -Blueprint $blueprint -Name 'roleOwner' -ArtifactFile .\artifacts\roleOwner.json</code></pre><h2 id="Publishing-blueprint"><a href="#Publishing-blueprint" class="headerlink" title="Publishing blueprint"></a>Publishing blueprint</h2><p>now that all the artifacts and policies have been added to the blueprint it is time to finally publish the blueprint and make it available for assignment</p>
<pre><code>Publish-AzBlueprint -Blueprint $blueprint -Version '1.0'</code></pre><p>this will publish the blueprint with version 1.0. The version can be set to anything. But for this example we set it to version 1.0</p>
<h2 id="Assigning-blueprint"><a href="#Assigning-blueprint" class="headerlink" title="Assigning blueprint"></a>Assigning blueprint</h2><p>The blueprint is published and ready to be assigned!</p>
<p>Blueprint assignment works the same as previous steps. So we first have to create an assignment.json file that contains our assignment details:</p>
<pre><code>{
"properties": {
"blueprintId": "<id of blueprint>",
"resourceGroups": {
"storageRG": {
"name": "<name of the storage account>",
"location": "<location of the storage account>"
}
},
"parameters": {
"storageAccountType": {
"value": "Standard_GRS"
},
"tagName": {
"value": "CostCenter"
},
"tagValue": {
"value": "ContosoIT"
},
"contributors": {
"value": [
"<objectId of the principal from Azure Active Directory>"
]
},
"owners": {
"value": [
"<objectId of the principal from Azure Active Directory>"
]
}
}
},
"identity": {
"type": "systemAssigned"
},
"location": "westus"
}</code></pre><p>PowerShell command the assign the blueprint</p>
<figure class="highlight ps"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">New-AzBlueprintAssignment -Blueprint <span class="variable">$blueprint</span> -Name <span class="string">'assignMyBlueprint'</span> -AssignmentFile .\blueprintAssignment.json</span><br></pre></td></tr></table></figure>
<h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion "></a>Conclusion </h2><p>Azure Blueprints makes it possible to create a repeatable set of Azure Resources that adhere to the rules and requirements of an organization. It can be used to keep track of what is deployed. What version is deployed where and makes it really easy to update a rule if required. Azure Blueprints also comes with an SDK and API, which gives you the chance to automate this process.</p>
</div>
<footer class="article-footer">
<a data-url="https://cloudrepublic.github.io/2020/03/04/Azure-Blueprints/" data-id="ckasa0xhe0000awun2354361j" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Azure/" rel="tag">Azure</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Azure-Blueprints/" rel="tag">Azure Blueprints</a></li></ul>
</footer>
</div>
</article>
<article id="post-kubernetes" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/03/04/kubernetes/" class="article-date">
<time datetime="2020-03-04T11:00:00.000Z" itemprop="datePublished">2020-03-04</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/03/04/kubernetes/">Kubernetes, Nginx ingress controller en Let's Encrypt op AKS</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<h2 id="Case-omschrijving"><a href="#Case-omschrijving" class="headerlink" title="Case omschrijving"></a>Case omschrijving</h2><p>In deze blog ga ik uitleggen wanneer je Kubernetes kan gebruiken, hoe je ermee kunt starten op AKS en hoe je een applicatie kunt uitrollen. Als voorbeeld gaan we een wordpress applicatie uitrollen op Kubernetes.</p>
<h2 id="Waarom-heb-ik-Kubernetes-nodig"><a href="#Waarom-heb-ik-Kubernetes-nodig" class="headerlink" title="Waarom heb ik Kubernetes nodig"></a>Waarom heb ik Kubernetes nodig</h2><p>Ik hoor vaak het argument waarom zou ik Kubernetes gebruiken wij hebben alles via <a href="https://en.wikipedia.org/wiki/Platform_as_a_service" target="_blank" rel="noopener">Paas</a> en <a href="https://en.wikipedia.org/wiki/Function_as_a_service" target="_blank" rel="noopener">Faas</a>. Wij vanuit Cloud Republic kiezen ook meestal voor een serverless architectuur. Serverless is lekker schaalbaar, gemakkelijk om mee te starten en goedkoop. Nu is Kubernetes ook geen vervanging voor Paas of Faas maar het is een toevoeging aan je toolbox. </p>
<img src="/images/kubernetes_hamer.jpeg" />
<p><strong><p style="text-align: center;">“Als je alleen een hamer hebt, neig je ernaar elk probleem te zien als een spijker.”</p></strong><br>Je kan een hele hoop oplossingen kwijt in Paas en Faas maar niet voor alles is Paas of Faas de juiste oplossing bijv.</p>
<ul>
<li>Als je complexe architecturen hebt kan dit een uitdaging zijn. Als je bijv. software moet installeren op de host om je applicatie werkend te maken of odbc drivers, speciale versies van frameworks nodig hebt of applicaties zoals een Jenkins server of een Zalenium test platform moet hosten.</li>
<li>Als je volledige controle wilt hebben over je infrastructuur. Je wilt bijv. controlle over de manier van schalen of het maximum aantal instanties. of je wilt mischien een blue-green deployment doen. <a href="https://en.wikipedia.org/wiki/Blue-green_deployment" target="_blank" rel="noopener">Blue-green deployment</a> </li>
<li>Als je legacy applicaties wilt hosten.</li>
<li>Als je geen vendor lock wilt hebben. Je kan je containers gemakkelijk verplaatsen naar een andere cloudprovider of naar je eigen datacenter</li>
<li>Kubernetes is ook mogelijk in je eigen datacentrum. Mag je data niet in de cloud staan dan is dit zeker een goed altenatief.</li>
<li>De applicaties zijn schaalbaar tot … instanties. Op een paas omgeving kun je standaard niet verder schalen dan 20 instanties in serverless is standaard het maximum instanties gelimiteerd tot 200.</li>
<li>Je hebt een standaard deploy methode voor elke applicatie. Het maakt niet uit of je een Nodejs, een .Net Core of een Java applicatie of een standaard CMS systeem. De deploy methode is altijd het uitrollen van een container welke alle dependencies bevat.</li>
</ul>
<h2 id="Wat-is-Kubernetes"><a href="#Wat-is-Kubernetes" class="headerlink" title="Wat is Kubernetes"></a>Wat is Kubernetes</h2><p>Kubernetes, ook wel k8s genoemd, is kort gezegd een open-source systeem beheren van grote groepen containers en containerized applicaties. Met de software zijn containers te groeperen en eenvoudig(er) te beheren. Kubernetes kan je onder andere helpen met de volgende zaken:</p>
<h4 id="Service-discovery-en-loadbalancing"><a href="#Service-discovery-en-loadbalancing" class="headerlink" title="Service discovery en loadbalancing"></a>Service discovery en loadbalancing</h4><p>Kubernetes kan de load van applicaties verdelen over de verschillende instanties van de applicatie zodat de load verdeelt wordt over de verschillende instanties. Als je applicatie gaat schalen zullen de nieuwe instanties worden toegevoegd aan de interne dns server en het binnenkomend verkeer wordt verdeelt over de nieuwe instanties. </p>
<p>Elke pod waar die een poort heeft gepubliseerd zal bereikbaar zijn doormiddel van een service bijv. een database server. Deze services worden toegevoegd aan een service registry. Als dan een pod een verbinding wil maken met de database pod dan gaat dit via de service. Aan het service registry zal gevraagd worden naar welk endpoint er verbonden dient te worden. Mocht er een service bij komen of mochten er meer pods beschikbaar zijn door een schaling worden deze toegevoegd en mocht er een niet meer werken dan wordt de service registry bijgewerkt. </p>
<img src="/images/kubernetes_service_discovery.png" />
<h4 id="Storage-orchestration"><a href="#Storage-orchestration" class="headerlink" title="Storage orchestration"></a>Storage orchestration</h4><p>In Kubernetes heb je de mogelijkheid om een gedeelde storage in te stellen op je cluster. Stel je voor dat je een MySql server draait dan wil je niet dat als je MySql container stuk gaat dat je database weg is omdat deze werd opgeslagen in je container.<br>Data welke belangerijk is en die je niet kwijt wil moet je niet in je container opslaan. Je kan hiervoor een persistant volume aanmaken en dit kun je koppelen aan een folder in je container. Zodoende als je container gaat schalen of hij werkt niet meer staan je database bestanden op een plaats buiten je container en kunnen de eventuele nieuwe containers ook bij de bestanden. Tevens is het gemakkelijker om je bestanden te backuppen op een gedeelde storage dan dat het een backup moet maken in verschillende containers.</p>
<p>Er zijn veel providers welke ondersteuning bieden aan Kubernetes:</p>
<ul>
<li>awsElasticBlockStore</li>
<li>azureDisk</li>
<li>azureFile</li>
<li>gcePersistentDisk</li>
<li>iscsi</li>
<li>local</li>
<li>nfs</li>
</ul>
<p>Voor een complete lijst kijk op <a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes" target="_blank" rel="noopener">Types of Persistent Volumes</a></p>
<img src="/images/kubernetes_storage.png" />
<h4 id="Geautomatiseerd-applicaties-updaten-en-terug-rollen"><a href="#Geautomatiseerd-applicaties-updaten-en-terug-rollen" class="headerlink" title="Geautomatiseerd applicaties updaten en terug rollen"></a>Geautomatiseerd applicaties updaten en terug rollen</h4><p>In Kubernetes kun je geautomatiseerd je applicaties updaten zonder downtime. Je kan opgeven hoeveel pods er offline mogen zijn tijdens een update van de applicatie. Je kunt ook opgeven dat er extra pods moeten worden aangemaakt tijdens de update zodoende blijft je applicatie op de gewenste hoeveelheid instanties.<br>Mocht de nieuwe versie toch niet goed zijn is deze gemakkelijk terug te rollen. Kubernetes houd een history bij van de gedeployde versies.</p>
<img src="/images/kubernetes_rollout.png" />
<h4 id="Automatische-verdeling-van-resources"><a href="#Automatische-verdeling-van-resources" class="headerlink" title="Automatische verdeling van resources"></a>Automatische verdeling van resources</h4><p>Kubernetes verdeeld automatisch de containers over de nodes gebaseerd op de recourse requirements en de beschikbaarheid op de nodes. Nodes kunnen worden voorzien van labels zodat alleen bepaalde workloads daar mogen draaien. Dit is iets anders als loadbalancing, bij loadbalancing wordt de load verdeelt over meerder pods maar deze zouden best op dezelfde node kunnen staan hierdoor zou een node het een stuk drukker krijgen als een andere node in je cluster.</p>
<img src="/images/kubernetes_placements.png" />
<h4 id="Automatisch-herstellend"><a href="#Automatisch-herstellend" class="headerlink" title="Automatisch herstellend"></a>Automatisch herstellend</h4><p>Mocht er een workload niet meer goed functioneren dan kan Kubernetes zelf een nieuwe versie van de pod opstarten. Of de pod nog goed functioneert kan op meerdere manieren gecontroleerd worden.</p>
<p>In een Dockerfile is een ENTRYPOINT gedefinieerd zodra dit process niet meer beschikbaar is zal de pod gestopt worden.</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">FROM mcr.microsoft.com/dotnet/core/runtime:3.1</span><br><span class="line">COPY --from=build-env /app/out .</span><br><span class="line"></span><br><span class="line"># Start</span><br><span class="line">ENTRYPOINT ["dotnet", "AuditlogService.dll"]</span><br></pre></td></tr></table></figure>
<p>In een pod definitie kun je een livenessProbe instellen met bijv. een url welke gecontroleerd word als de response van de URL anders is dan een status code 200 zal de pod als unhealthy worden gezien.</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> creationTimestamp:</span> <span class="literal">null</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> run:</span> <span class="string">auditlogservice</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">auditlogservice</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"><span class="attr"> containers:</span></span><br><span class="line"><span class="attr"> - image:</span> <span class="string">marcoippel/auditlogservice:1.0</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">auditlogservice</span></span><br><span class="line"><span class="attr"> resources:</span> <span class="string">{}</span></span><br><span class="line"><span class="attr"> livenessProbe:</span></span><br><span class="line"><span class="attr"> httpGet:</span></span><br><span class="line"><span class="attr"> path:</span> <span class="string">/health</span></span><br><span class="line"><span class="attr"> readinessProbe:</span></span><br><span class="line"><span class="attr"> httpGet:</span></span><br><span class="line"><span class="attr"> path:</span> <span class="string">/ready</span></span><br><span class="line"><span class="attr"> dnsPolicy:</span> <span class="string">ClusterFirst</span></span><br><span class="line"><span class="attr"> restartPolicy:</span> <span class="string">Never</span></span><br><span class="line"><span class="attr">status:</span> <span class="string">{}</span></span><br></pre></td></tr></table></figure>
<p><img src="/images/Kubernetes_self_healing.png" /></p>
<h4 id="Secret-en-configuration-beheer"><a href="#Secret-en-configuration-beheer" class="headerlink" title="Secret en configuration beheer"></a>Secret en configuration beheer</h4><p>In Kubernetes kun je secrets en configuraties aanmaken welke dan gebruikt kunnen worden in de applicaties. Deze objecten zijn op alle nodes beschikbaar en worden beheerd door Kubernetes. Secrets en configuraties kunnen worden uitgelezen als environment variabelen of als een volume worden gemount in de pod.</p>
<img src="/images/kubernetes_secret.png" />
<h2 id="Hoe-begin-je-met-Kubernetes"><a href="#Hoe-begin-je-met-Kubernetes" class="headerlink" title="Hoe begin je met Kubernetes"></a>Hoe begin je met Kubernetes</h2><p>Het is aan te raden om een managed instantie van Kubernetes af te nemen bij een public cloud provider. Bij een managed Kubernetes instantie hoef je je niet meer druk te maken over de installatie en configuratie van het cluster. Het kost namelijk een hele hoop tijd om een goed werkend en een veilig cluster te bouwen. Je moet het cluster blijven monitoren of het nog goed werkt en zelf alerts instellen om op de hoogte gehouden te worden als het cluster niet goed functioneert. Omdat Kubernetes zo uitgebreidt is kan er ook ontzettend veel misgaan en dan moet je het zelf troubleshooten en oplossen.</p>
<p>Er zijn heel veel varianten van Kubernetes te krijgen enkele voorbeelden zijn:</p>
<ul>
<li>Azure Kubernetes Service (AKS)</li>
<li>Amazon Elastic Kubernetes Service (EKS)</li>
<li>Google Kubernetes Engine (GKE)</li>
</ul>
<p>Ik ga het hier verder over AKS hebben dit is de managed Kubernetes oplossing van Azure.</p>
<h2 id="Wat-is-AKS"><a href="#Wat-is-AKS" class="headerlink" title="Wat is AKS"></a>Wat is AKS</h2><p>AkS staat voor Azure Kubernetes Service en is een managed service van Azure om je Kubernetes workload op te draaien. AKS is volledig in Azure geïntegreerd het maakt bijvoorbeeld gebruik van Azure monitoring en alerting. Je kunt bijvoorbeeld zien wat de status is van je cluster en hoe je containers draaien. Je kunt op container niveau inloggen en de logfiles per container bekijken. Ook kan je door middel van een terminal direct inloggen op de container. Azure DevOps heeft een hele goede integratie met AKS wat het mogelijk maakt om gemakkelijk applicaties te deployen op je Kubernetes cluster. In een andere blog zal ik in detail ingaan op devops in combinatie met Kubernetes.</p>
<img src="/images/Kubernetes_azure_monitor.png" />
Een overzicht van de monitoring in AKS.
<h2 id="Hoe-installeer-je-AKS"><a href="#Hoe-installeer-je-AKS" class="headerlink" title="Hoe installeer je AKS"></a>Hoe installeer je AKS</h2><p>AKS kun je volledig installeren door middel van de azure cli, voer de onderstaande commando’s uit om AKS te installeren.</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># maak een resource groep aan.</span></span><br><span class="line">az group create --name kubernetesdemo --location west-europe</span><br><span class="line"></span><br><span class="line"><span class="comment"># maak een kubernetes cluster aan met twee workers.</span></span><br><span class="line">az aks create --resource-group kubernetesdemo --name demo --node-count 2 --<span class="built_in">enable</span>-addons monitoring --generate-ssh-keys</span><br><span class="line"></span><br><span class="line"><span class="comment"># installeer kubectl als deze nog niet id geinstalleerd</span></span><br><span class="line">az aks install-cli</span><br><span class="line"></span><br><span class="line"><span class="comment"># haal de credentials op van je cluster en voeg deze toe aan je kubectl config</span></span><br><span class="line">az aks get-credentials --resource-group kubernetesdemo --name demo</span><br></pre></td></tr></table></figure>
<p>Nu ben je klaar om applicaties te deployen op Kubernetes.</p>
<h2 id="Hoe-deploy-je-applicaties-op-Kubernetes"><a href="#Hoe-deploy-je-applicaties-op-Kubernetes" class="headerlink" title="Hoe deploy je applicaties op Kubernetes"></a>Hoe deploy je applicaties op Kubernetes</h2><p>Kubernetes bestaat uit verschillende componenten, namelijk:</p>
<ul>
<li>Een ingresscontroller is een soort van reverse proxy welke het verkeer op basis van de inkomende url het verkeer naar een bepaalde service kan sturen.</li>
<li>Een service is een object wat een pod exposed naar buiten. De reden dat je hier een apart object voor gebruikt wordt is dat als je een pod connect op het ipadres en de pod wordt verplaatst naar een andere node dan krijg je een nieuw ipadress.</li>
<li>Een pod is een wrapper om een of meerdere containers.</li>
<li>Een persistant volume claim is een reservering op een persistant volume.</li>
<li>Een persistant volume is een stuk storage wat beschikbaar is gesteld door een administrator.</li>
<li>Een secret is een object waar je gevoelige informatie opslaan en beheren, zoals wachtwoorden, OAuth-tokens en ssh-sleutels.</li>
</ul>
<p>Om componenten uit te rollen op Kubernetes moeten we deze beschrijven in yaml. Deze yaml zal vervolgens worden geserialiseerd naar Kubernetes objecten.<br>Om een voorbeeld wordpress applicatie uit te rollen hebben we de volgende yaml nodig:</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Namespace</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> creationTimestamp:</span> <span class="literal">null</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr">spec:</span> <span class="string">{}</span></span><br><span class="line"><span class="attr">status:</span> <span class="string">{}</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">extensions/v1beta1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Ingress</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">demo</span> </span><br><span class="line"><span class="attr"> namespace:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> annotations:</span></span><br><span class="line"> <span class="string">kubernetes.io/ingress.class:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"><span class="attr"> rules:</span></span><br><span class="line"><span class="attr"> - host:</span> <span class="string">demo-kubernetes.westeurope.cloudapp.azure.com</span></span><br><span class="line"><span class="attr"> http:</span></span><br><span class="line"><span class="attr"> paths:</span></span><br><span class="line"><span class="attr"> - backend:</span></span><br><span class="line"><span class="attr"> serviceName:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> servicePort:</span> <span class="number">80</span></span><br><span class="line"><span class="attr"> path:</span> <span class="string">/</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">data:</span></span><br><span class="line"><span class="attr"> password:</span> <span class="string">d2Vsa29tMDE=</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Secret</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> creationTimestamp:</span> <span class="literal">null</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">mysql-pass</span></span><br><span class="line"><span class="attr"> namespace:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Service</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">wordpress-mysql</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> namespace:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"><span class="attr"> ports:</span></span><br><span class="line"><span class="attr"> - port:</span> <span class="number">3306</span></span><br><span class="line"><span class="attr"> selector:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> tier:</span> <span class="string">mysql</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">PersistentVolumeClaim</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">mysql-pv-claim</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> namespace:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"><span class="attr"> accessModes:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">ReadWriteOnce</span></span><br><span class="line"><span class="attr"> resources:</span></span><br><span class="line"><span class="attr"> requests:</span></span><br><span class="line"><span class="attr"> storage:</span> <span class="number">20</span><span class="string">Gi</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">apps/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Deployment</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">wordpress-mysql</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> namespace:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"><span class="attr"> selector:</span></span><br><span class="line"><span class="attr"> matchLabels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> tier:</span> <span class="string">mysql</span></span><br><span class="line"><span class="attr"> strategy:</span></span><br><span class="line"><span class="attr"> type:</span> <span class="string">Recreate</span></span><br><span class="line"><span class="attr"> template:</span></span><br><span class="line"><span class="attr"> metadata:</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> tier:</span> <span class="string">mysql</span></span><br><span class="line"><span class="attr"> spec:</span></span><br><span class="line"><span class="attr"> containers:</span></span><br><span class="line"><span class="attr"> - image:</span> <span class="attr">mysql:5.6</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">mysql</span></span><br><span class="line"><span class="attr"> env:</span></span><br><span class="line"><span class="attr"> - name:</span> <span class="string">MYSQL_ROOT_PASSWORD</span></span><br><span class="line"><span class="attr"> valueFrom:</span></span><br><span class="line"><span class="attr"> secretKeyRef:</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">mysql-pass</span></span><br><span class="line"><span class="attr"> key:</span> <span class="string">password</span></span><br><span class="line"><span class="attr"> ports:</span></span><br><span class="line"><span class="attr"> - containerPort:</span> <span class="number">3306</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">mysql</span></span><br><span class="line"><span class="attr"> volumeMounts:</span></span><br><span class="line"><span class="attr"> - name:</span> <span class="string">mysql-persistent-storage</span></span><br><span class="line"><span class="attr"> mountPath:</span> <span class="string">/var/lib/mysql</span></span><br><span class="line"><span class="attr"> volumes:</span></span><br><span class="line"><span class="attr"> - name:</span> <span class="string">mysql-persistent-storage</span></span><br><span class="line"><span class="attr"> persistentVolumeClaim:</span></span><br><span class="line"><span class="attr"> claimName:</span> <span class="string">mysql-pv-claim</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Service</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> namespace:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"><span class="attr"> ports:</span></span><br><span class="line"><span class="attr"> - port:</span> <span class="number">80</span></span><br><span class="line"><span class="attr"> selector:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> tier:</span> <span class="string">frontend</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">PersistentVolumeClaim</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">wp-pv-claim</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> namespace:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"><span class="attr"> accessModes:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">ReadWriteOnce</span></span><br><span class="line"><span class="attr"> resources:</span></span><br><span class="line"><span class="attr"> requests:</span></span><br><span class="line"><span class="attr"> storage:</span> <span class="number">20</span><span class="string">Gi</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">apps/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Deployment</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> namespace:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"><span class="attr"> selector:</span></span><br><span class="line"><span class="attr"> matchLabels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> tier:</span> <span class="string">frontend</span></span><br><span class="line"><span class="attr"> strategy:</span></span><br><span class="line"><span class="attr"> type:</span> <span class="string">Recreate</span></span><br><span class="line"><span class="attr"> template:</span></span><br><span class="line"><span class="attr"> metadata:</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> tier:</span> <span class="string">frontend</span></span><br><span class="line"><span class="attr"> spec:</span></span><br><span class="line"><span class="attr"> containers:</span></span><br><span class="line"><span class="attr"> - image:</span> <span class="attr">wordpress:5.3.2-apache</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> env:</span></span><br><span class="line"><span class="attr"> - name:</span> <span class="string">WORDPRESS_DB_HOST</span></span><br><span class="line"><span class="attr"> value:</span> <span class="string">wordpress-mysql</span></span><br><span class="line"><span class="attr"> - name:</span> <span class="string">WORDPRESS_DB_PASSWORD</span></span><br><span class="line"><span class="attr"> valueFrom:</span></span><br><span class="line"><span class="attr"> secretKeyRef:</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">mysql-pass</span></span><br><span class="line"><span class="attr"> key:</span> <span class="string">password</span></span><br><span class="line"><span class="attr"> ports:</span></span><br><span class="line"><span class="attr"> - containerPort:</span> <span class="number">80</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">wordpress</span></span><br><span class="line"><span class="attr"> volumeMounts:</span></span><br><span class="line"><span class="attr"> - name:</span> <span class="string">wordpress-persistent-storage</span></span><br><span class="line"><span class="attr"> mountPath:</span> <span class="string">/var/www/html</span></span><br><span class="line"><span class="attr"> volumes:</span></span><br><span class="line"><span class="attr"> - name:</span> <span class="string">wordpress-persistent-storage</span></span><br><span class="line"><span class="attr"> persistentVolumeClaim:</span></span><br><span class="line"><span class="attr"> claimName:</span> <span class="string">wp-pv-claim</span></span><br></pre></td></tr></table></figure>
<p>We kunnen deze yaml uitvoeren door het volgende commando uit te voeren:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl apply -f wordpress.yaml</span><br></pre></td></tr></table></figure>
<p>Dit commando zal de yaml naar de api sturen van Kubernetes en de objecten aanmaken of updaten. Als alles goed is gegaan hebben we de onderstaande applicatie uitgerold:</p>
<img src="/images/kubernetes_wordpress_overview.png" />
<h2 id="Moet-ik-al-die-yaml-zelf-typen"><a href="#Moet-ik-al-die-yaml-zelf-typen" class="headerlink" title="Moet ik al die yaml zelf typen ?"></a>Moet ik al die yaml zelf typen ?</h2><p>De wordpress applicatie bestaat uit ongeveer 171 regels yaml code welke ook nog eens de juiste manier moet inspringen moet je dat echt allemaal zelf typen? Nou nee gelukkig niet, je kunt het grootste deel van de yaml laten genereren. Als voorbeeld nemen we een deployment object.</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl create deployment my-dep --image=busybox -o yaml --dry-run > deployment.yaml</span><br></pre></td></tr></table></figure>
<p>We maken hier een deployment aan met als naam my-dep en als image gebruiken we busybox. We doen een dry-run zodat we niets aanmaken en doen een output naar yaml. Dit alles schrijven we weg in een deployment.yaml bestand. Dit kunnen we doen voor al de objecten welke we nodig hebben.</p>
<p>Het resultaat ziet er als volgt uit:</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">apps/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Deployment</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"><span class="attr"> creationTimestamp:</span> <span class="literal">null</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">my-dep</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">my-dep</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"><span class="attr"> replicas:</span> <span class="number">1</span></span><br><span class="line"><span class="attr"> selector:</span></span><br><span class="line"><span class="attr"> matchLabels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">my-dep</span></span><br><span class="line"><span class="attr"> strategy:</span> <span class="string">{}</span></span><br><span class="line"><span class="attr"> template:</span></span><br><span class="line"><span class="attr"> metadata:</span></span><br><span class="line"><span class="attr"> creationTimestamp:</span> <span class="literal">null</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="attr"> app:</span> <span class="string">my-dep</span></span><br><span class="line"><span class="attr"> spec:</span></span><br><span class="line"><span class="attr"> containers:</span></span><br><span class="line"><span class="attr"> - image:</span> <span class="string">busybox</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">busybox</span></span><br><span class="line"><span class="attr"> resources:</span> <span class="string">{}</span></span><br><span class="line"><span class="attr">status:</span> <span class="string">{}</span></span><br></pre></td></tr></table></figure>
<h2 id="Welke-resources-zijn-handig-om-te-bekijken"><a href="#Welke-resources-zijn-handig-om-te-bekijken" class="headerlink" title="Welke resources zijn handig om te bekijken"></a>Welke resources zijn handig om te bekijken</h2><p>Ik luister graag in de auto naar podcasts en een hele goede podcast is die van Bret Fisher. Hij is een docker captain en hij heeft elke week een live stream op youtube en hij maakt hier ook podcasts van alles gaat over docker, docker swarm en Kubernetes. <a href="https://www.bretfisher.com/podcast/" target="_blank" rel="noopener">https://www.bretfisher.com/podcast/</a></p>
<p>Als je wilt beginnen met Kubernetes op je computer dan is k3d een hele goede optie. Het is een gestripte versie van Kubernetes met een hele handige installer erbij. <a href="https://github.com/rancher/k3d" target="_blank" rel="noopener">https://github.com/rancher/k3d</a></p>
<p>Als je aan de slag wilt met Kubernetes en niet meteen een cluster op je pc wilt installeren dan kun je terecht op Katakoda. Het is een omgeving waar je voor een uur een tijdelijk cluster kunt starten. Er zijn ook korte cursussen aanwezig welke je dan kunt uitvoeren op het cluster. <a href="https://www.katacoda.com/" target="_blank" rel="noopener">https://www.katacoda.com/</a></p>
<p>Als je iets langer wilt spelen met Kubernetes dan kun je terecht op play with Kubernetes. Dit is ook een gevirtualiseerde omgeving welke 4 uur beschikbaar is. <a href="https://labs.play-with-k8s.com/" target="_blank" rel="noopener">https://labs.play-with-k8s.com/</a></p>
<p>Mocht je een cursus willen doen kan ik je echt de cursussen aanraden van KodeKloud op udemy. Dit zijn echt hele duidelijke cursussen en als bonus heb je toegang tot een online leeromgeving waar je allerlei opdrachten moet uitvoeren. <a href="https://www.udemy.com/course/certified-kubernetes-application-developer/" target="_blank" rel="noopener">https://www.udemy.com/course/certified-kubernetes-application-developer/</a></p>
<h2 id="Conclusie"><a href="#Conclusie" class="headerlink" title="Conclusie"></a>Conclusie</h2><p>Ondanks dat Kubernetes een enorme steile leercurve heeft is het eenmaal als je het door hebt een geweldig platform om je applicaties op te hosten. Je kunt er alle complexe applicaties op hosten maar je kunt er ook gewoon je Azure functions op hosten. Je hebt met Kubernetes een volledige gereedschapskist om al je oplossingen in te hosten. Met een hosted oplossing op bijv. AKS wordt al heel veel complexiteit uit handen genomen.</p>
</div>
<footer class="article-footer">
<a data-url="https://cloudrepublic.github.io/2020/03/04/kubernetes/" data-id="ckasa0xhu0003awunfc0a79gi" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/AKS/" rel="tag">AKS</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Kubernetes/" rel="tag">Kubernetes</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Let-s-Encrypt/" rel="tag">Let's Encrypt</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Nginx/" rel="tag">Nginx</a></li></ul>
</footer>
</div>
</article>
<article id="post-Docker-Swarm" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2019/11/03/Docker-Swarm/" class="article-date">
<time datetime="2019-11-03T11:00:00.000Z" itemprop="datePublished">2019-11-03</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2019/11/03/Docker-Swarm/">Docker Swarm, Traefik, Let's Encrypt en Portainer</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<h2 id="Case-omschrijving"><a href="#Case-omschrijving" class="headerlink" title="Case omschrijving "></a>Case omschrijving </h2><p>Niet lang geleden heb ik besloten om mij eens goed te gaan verdiepen in Docker. Je hoort het overal en het wordt steeds populairder en ik hoor steeds vaker dat het een goed alternatief is voor de huidige manier van werken namelijk Faas en Paas in een public cloud hierdoor ben ik nieuwsgierig geworden en wil ik eens kijken wat hier nu de voordelen van zijn. Ik ben begonnen om mij te certificeren, ik heb een cursus gevolgd en mijn examen met succes behaald. Ik vind het altijd belangrijk om te weten hoe iets in elkaar zit en waarom het zo in elkaar zit. Mijn idee is om een of meerdere productie applicaties op een Docker Swarm cluster te gaan draaien. In deze blog ga ik laten zien hoe ik een goede infrastructuur bouw om de applicaties te hosten.</p>
<p>De omgeving moest aan de volgende voorwaarden voldoen:</p>
<ul>
<li>Er moeten meerdere web applicaties op kunnen draaien welke op poort 80 en 443 benaderd kunnen worden.</li>
<li>Alle web applicaties moeten draaien op SSL, dit moet mij zo min mogelijk werk kosten.</li>
<li>De oplossing moet schaalbaar zijn en bestand zijn tegen het uitvallen en/of offline gaan van servers.</li>
<li>Ik wil de oplossing kunnen hosten bij een Cloud provider, Hosting provider of lokaal om te kunnen testen.</li>
<li>Het beheren van de containers moet gemakkelijk en overzichtelijk zijn.</li>
<li>Aangezien ik een echte Hollander ben moeten de kosten ook een beetje beperkt blijven.</li>
</ul>
<p>De oplossing welke ik heb gemaakt is een combinatie van Docker Swarm, Traefik, Let’s Encrypt en Portainer.</p>
<h2 id="Wat-is-Docker-Swarm"><a href="#Wat-is-Docker-Swarm" class="headerlink" title="Wat is Docker Swarm"></a>Wat is Docker Swarm</h2><p>Docker Swarm is een tool waarmee je Docker-containers kunt beheren en schalen. Als je Docker installeert dan krijg je daar meteen Swarm bij. Docker Swarm is een standaard product van Docker wat bij elke installatie van Docker wordt meegeleverd. Met Docker Swarm kun je een cluster bouwen van verschillende virtuele machines deze worden hierna nodes genoemd. Hierop kunnen de Docker-containers worden gedeployed als een stack[^2] of een service[^1]. Als je een cluster wilt hebben wat een hoge beschikbaarheid heeft wordt aangeraden om minstens 3 manager nodes te hebben. Docker Swarm maakt namelijk gebruik van het Raft Consensus Algoritme, 1 manager is de leider van de swarm en de status van de manager wordt gesynchroniseerd over de overige managers. Mocht de leider niet meer beschikbaar zijn om wat voor reden dan ook kan een andere manager zijn taken over nemen.</p>
<p>Om te berekenen hoeveel managers er mogen uitvallen voordat het cluster niet meer kan functioneren wordt de volgende berekening gebruikt:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">X = (N-1)/2</span><br></pre></td></tr></table></figure></p>
<p>Bij dus een cluster van 3 manager mag er 1 manager uitvallen en zal het cluster nog steeds functioneren. Docker adviseert om niet meer dan 7 managers te gebruiken om performance issues met het synchroniseren te voorkomen. Voor meer informatie zie <a href="https://docs.docker.com/engine/swarm/raft/" target="_blank" rel="noopener">https://docs.docker.com/engine/swarm/raft/</a></p>
<p>Om een Docker Swarm cluster op te zetten kun je de volgende stappen uitvoeren:</p>
<ul>
<li>Installeer 5 virtuele machine met bijv. Ubuntu server. Voor de 3 managers hebben we niet hele zware virtuele machines nodig. Een basic A1 1.75 GB RAM volstaat al voor een manager node. Voor de 2 worker nodes zou ik kiezen voor een virtuele machine met 8 GB RAM. </li>
<li>Installeer Docker community edition op alle 5 de servers. Volg de handleiding op <a href="https://docs.docker.com/install/linux/docker-ce/ubuntu/" target="_blank" rel="noopener">https://docs.docker.com/install/linux/docker-ce/ubuntu/</a>. </li>
<li>Op 1 van de manager servers voer het volgende commando uit om Docker Swarm te initialiseren.</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">$ docker swarm init --advertise-addr "public ipadres van de server"</span><br><span class="line">Swarm initialized: current node (bvz81updecsj6wjz393c09vti) is now a manager.</span><br><span class="line"></span><br><span class="line">To add a worker to this swarm, run the following command:</span><br><span class="line"></span><br><span class="line"> docker swarm join \npm install hexo --save</span><br><span class="line"> --token SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-1awxwuwd3z9j1z3puu7rcgdbx \</span><br><span class="line"> 172.17.0.2:2377</span><br><span class="line"></span><br><span class="line">To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.</span><br></pre></td></tr></table></figure>
<ul>
<li>Voer het bovenstaande docker swarm join token uit op de 2 nodes. </li>
<li>Voer het commando docker swarm join-token manager uit en voer het join commando uit op de overige 2 managers.</li>
</ul>
<p>Als dit klaar gedaan is heb je een Docker Swarm cluster gemaakt zoals in het onderstaande overzicht is weergegeven.</p>
<p><img src="/images/Docker-Swarm-overview.png"><br>Mocht je meer details willen hebben over het opzetten van een Docker Swarm cluster kijk dan op: <a href="https://docs.docker.com/engine/swarm/swarm-tutorial/create-swarm/" target="_blank" rel="noopener">https://docs.docker.com/engine/swarm/swarm-tutorial/create-swarm/</a></p>
<p>Docker Swarm zal niet automatisch schalen als de load op je applicatie hoger wordt, wil je meer containers uitrollen van je applicatie kun je dit doen door meer replica’s aan te maken. Als je bijv. de service api_api wilt opschalen naar 5 instanties kun je dit doen door middel van het volgende commando:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker service scale api_api=5</span><br></pre></td></tr></table></figure>
<p>Je kunt de schaling ook regelen in de UI van Portainer. Verderop in het artikel ga ik dieper in op hoe en wat Portainer is.<br><img src="/images/Docker-Swarm-scaling.png"></p>
<p>Mocht je nu toch te weinig capaciteit hebben in je cluster kun je eenvoudig een nieuwe virtuele machine inrichten met Ubuntu en Docker erop installeren. Hierna voer je het Docker Swarm join commando uit op de server en deze zal het bestaande cluster uitbreiden met de extra capaciteit. Om inzicht te krijgen in de performance van het cluster zijn monitoring applicaties beschikbaar, een van de bekendere is Prometheus. Het gaat in deze blog te ver om de ins en outs van Prometheus te behandelen. Mocht je meer informatie willen hebben over Prometheus zie <a href="https://prometheus.io/" target="_blank" rel="noopener">https://prometheus.io/</a>.</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker swarm join \</span><br><span class="line"> --token SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-1awxwuwd3z9j1z3puu7rcgdbx \</span><br><span class="line"> 172.17.0.2:2377</span><br></pre></td></tr></table></figure>
<p>Mocht je Docker Swarm op Azure hebben uitgerold kun je gebruik maken van een vm scale sets om automatisch nodes bij je cluster te zetten. Ik heb hier verder nog geen ervaring mee maar ga hier zeker mee aan de slag.</p>
<p>Omdat ik Docker Swarm gebruik kan ik de volgende punten afvinken van mijn lijstje:</p>
<ul>
<li>De oplossing moet schaalbaar zijn en bestand zijn tegen het uitvallen of offline gaan van servers.<br> – In het cluster zitten 3 managers dus er kan er 1 offline gaan volgens de berekening: “3 managers - 1 = 2 2/2 = 1.<br> – Ook hebben we 2 workers welke de containers kunnen hosten. Mocht er 1 offline gaan worden de containers opnieuw gestart op de andere node.<br> – De manager heeft onder andere als taak om er voor te zorgen dat de containers draaien op 1 of meerdere nodes.</li>
<li>Ik wil de oplossing kunnen hosten bij een Cloud provider, Hosting provider of lokaal om te kunnen testen.<br> – Omdat Docker Swarm standaard in elke Docker installatie zit kan het zonder verdere installatie van tools gebruikt worden overal waar je Docker hebt geïnstalleerd. </li>
</ul>
<h2 id="Wat-is-Let’s-Encrypt"><a href="#Wat-is-Let’s-Encrypt" class="headerlink" title="Wat is Let’s Encrypt"></a>Wat is Let’s Encrypt</h2><p>Let’s Encrypt is een certificaatautoriteit opgericht op 16 april 2016. Het geeft X.509 certificaten uit voor het Transport Layer Security (TLS) encryptie-protocol, zonder dat dit kosten met zich meebrengt. De certificaten worden uitgegeven via een geautomatiseerd proces dat is ontworpen om het tot nu toe complexe proces van handmatige validatie, ondertekening, installatie en hernieuwing van certificaten voor beveiligde websites te elimineren. (<a href="https://nl.wikipedia.org/wiki/Let%27s_Encrypt" target="_blank" rel="noopener">Wikipedia</a>)</p>
<p>Voor meer informatie over Let’s Encrypt zie <a href="https://letsencrypt.org/" target="_blank" rel="noopener">https://letsencrypt.org/</a></p>
<h2 id="Wat-is-Traefik"><a href="#Wat-is-Traefik" class="headerlink" title="Wat is Traefik"></a>Wat is Traefik</h2><p>Traefik is een opensource router welke speciaal is ontworpen voor container oplossingen. Traefik wordt als global service op elke manager gedeployed op het cluster. Dit wil zeggen elke node met als rol manager krijgt een Traefik container. De reden dat Traefik op de manager nodes gedeployed dient te worden is dat de Docker api wordt uitgelezen. Zodra er een container bij komt en deze is geconfigureerd met de Traefik labels kan Traefik de labels van de container uitlezen en een virtuele host aanmaken voor de container en een SSL-certificaat aanvragen bij Let’s Encrypt. Zodoende is de container beschikbaar voor de buitenwereld met een SSL-certificaat.</p>
<p>Zie hier een voorbeeld Docker-Compose[^3] file om een Traefik container te deployen als stack[^2] op het Docker Swarm cluster.<br><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">'3.7'</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"><span class="attr"> traefik:</span></span><br><span class="line"><span class="attr"> image:</span> <span class="attr">traefik:1.7.13</span></span><br><span class="line"><span class="attr"> ports:</span></span><br><span class="line"><span class="attr"> - target:</span> <span class="number">80</span></span><br><span class="line"><span class="attr"> published:</span> <span class="number">80</span></span><br><span class="line"><span class="attr"> mode:</span> <span class="string">host</span></span><br><span class="line"><span class="attr"> - target:</span> <span class="number">443</span></span><br><span class="line"><span class="attr"> published:</span> <span class="number">443</span></span><br><span class="line"><span class="attr"> mode:</span> <span class="string">host</span></span><br><span class="line"><span class="attr"> command:</span> <span class="string">></span></span><br><span class="line"><span class="string"> --api</span></span><br><span class="line"><span class="string"> --acme</span></span><br><span class="line"><span class="string"> --acme.storage=/certs/acme.json</span></span><br><span class="line"><span class="string"> --acme.entryPoint=https</span></span><br><span class="line"><span class="string"> --acme.httpChallenge.entryPoint=http</span></span><br><span class="line"><span class="string"> --acme.onHostRule=true</span></span><br><span class="line"><span class="string"> --acme.onDemand=false</span></span><br><span class="line"><span class="string"> --acme.acmelogging=true</span></span><br><span class="line"><span class="string"> --acme.email=${EMAIL}</span></span><br><span class="line"><span class="string"> --docker</span></span><br><span class="line"><span class="string"> --docker.swarmMode</span></span><br><span class="line"><span class="string"> --docker.domain=${DOMAIN}</span></span><br><span class="line"><span class="string"> --docker.watch</span></span><br><span class="line"><span class="string"> --defaultentrypoints=http,https</span></span><br><span class="line"><span class="string"> --entrypoints='Name:http Address::80'</span></span><br><span class="line"><span class="string"> --entrypoints='Name:https Address::443 TLS'</span></span><br><span class="line"><span class="string"> --logLevel=DEBUG</span></span><br><span class="line"><span class="string"> --accessLog</span></span><br><span class="line"><span class="string"></span><span class="attr"> volumes:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">/var/run/docker.sock:/var/run/docker.sock</span></span><br><span class="line"><span class="attr"> - traefik_certs:</span><span class="string">/certs</span></span><br><span class="line"><span class="attr"> configs:</span></span><br><span class="line"><span class="attr"> - source:</span> <span class="string">traefik_htpasswd</span></span><br><span class="line"><span class="attr"> target:</span> <span class="string">/etc/htpasswd</span></span><br><span class="line"><span class="attr"> networks:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">public</span></span><br><span class="line"><span class="attr"> deploy:</span></span><br><span class="line"><span class="attr"> mode:</span> <span class="string">global</span></span><br><span class="line"><span class="attr"> replicas:</span> <span class="number">1</span></span><br><span class="line"><span class="attr"> placement:</span></span><br><span class="line"><span class="attr"> constraints:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">node.role</span> <span class="string">==</span> <span class="string">manager</span></span><br><span class="line"><span class="attr"> update_config:</span></span><br><span class="line"><span class="attr"> parallelism:</span> <span class="number">1</span></span><br><span class="line"><span class="attr"> delay:</span> <span class="number">10</span><span class="string">s</span></span><br><span class="line"><span class="attr"> restart_policy:</span></span><br><span class="line"><span class="attr"> condition:</span> <span class="string">on-failure</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">"traefik.docker.network=public"</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">"traefik.port=8080"</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">"traefik.backend=traefik"</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">"traefik.enable=true"</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">"traefik.frontend.rule=Host:traefik.${DOMAIN}"</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">"traefik.frontend.auth.basic.usersFile=/etc/htpasswd"</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">"traefik.frontend.headers.SSLRedirect=true"</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">"traefik.frontend.entryPoints=http,https"</span></span><br><span class="line"></span><br><span class="line"><span class="attr">configs:</span></span><br><span class="line"><span class="attr"> traefik_htpasswd:</span></span><br><span class="line"><span class="attr"> file:</span> <span class="string">./htpasswd</span></span><br><span class="line"></span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line"><span class="attr"> public:</span></span><br><span class="line"><span class="attr"> driver:</span> <span class="string">overlay</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">public</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line"><span class="attr"> traefik_certs:</span> <span class="string">{}</span></span><br></pre></td></tr></table></figure></p>
<p>Om de stack* uit te rollen voeren we het volgende commando uit om een manager node:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker stack deploy -c docker-compose.traefik.yml proxy</span><br></pre></td></tr></table></figure></p>
<p>Een kleine samenvatting wat er gebeurt in dit Docker-Compose[^3] bestand:</p>
<ul>
<li>We maken een container aan op basis van traefik:1.7.13.</li>
<li>We publiseren poort 80 en 443.</li>
<li>We regelen de Docker swarm en acme configuratie in.</li>
<li>We mounten 2 volumes.</li>
<li>We koppelen het wachtwoord bestand vanuit de config.</li>
<li>We zorgen dat het wordt uitgerold op elke manager in het cluster.</li>
<li>We regelen de webui van Traefik in.</li>
<li>We maken een netwerk aan genaamd Public van het type overlay. </li>
</ul>
<p>Voor meer informatie over Traefik zie <a href="https://traefik.io/" target="_blank" rel="noopener">https://traefik.io/</a></p>
<p>Omdat ik Traefik gebruik als reverse proxy kan ik de volgende punten afvinken van mijn lijstje:</p>
<ul>
<li>Er moeten meerdere web applicaties op kunnen draaien welke op poort 80 en 443 benaderd kunnen worden.<br> – Doordat Traefik de labels van containers kan uitlezen maakt het automatisch virtuele hosts aan.</li>
<li>Alle webapplicaties moeten draaien op ssl, dit moet mij zo min mogelijk werk kosten.<br> – Traefik ondersteund out of the box Let’s Encrypt SSL-certificaten.</li>
</ul>
<h2 id="Wat-is-Portainer"><a href="#Wat-is-Portainer" class="headerlink" title="Wat is Portainer"></a>Wat is Portainer</h2><p>Portainer is een opensource web interface om je Docker te beheren zowel lokaal als remote. Met Portainer kun je de volgende Docker concepten beheren: </p>
<ul>
<li>Containers</li>
<li>Images</li>
<li>Networks</li>
<li>Volumes</li>
<li>Services</li>
<li>Swarm Cluster</li>
</ul>
<p><img src="/images/Docker-Swarm-portainer-dashboard.png"></p>
<p>Portainer dient ook op een manager node geïnstalleerd te worden omdat Portainer ook via de Docker api het cluster beheerd. Tevens is er Portainer agent beschikbaar welke als global service gedeployed dient te worden op alle nodes zodat Portainer ook weet heeft welke containers op welke nodes draaien.</p>
<p>Zie hier een voorbeeld Docker-Compose[^3] file om een Portainer container en een Portainer agent te deployen als stack** op Docker Swarm.</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">'3.7'</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"><span class="attr"> agent:</span></span><br><span class="line"><span class="attr"> image:</span> <span class="string">portainer/agent</span></span><br><span class="line"><span class="attr"> environment:</span></span><br><span class="line"><span class="attr"> AGENT_CLUSTER_ADDR:</span> <span class="string">tasks.agent</span></span><br><span class="line"><span class="attr"> volumes:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">/var/run/docker.sock:/var/run/docker.sock</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">/var/lib/docker/volumes:/var/lib/docker/volumes</span></span><br><span class="line"><span class="attr"> networks:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">private</span></span><br><span class="line"><span class="attr"> deploy:</span></span><br><span class="line"><span class="attr"> mode:</span> <span class="string">global</span></span><br><span class="line"><span class="attr"> portainer:</span></span><br><span class="line"><span class="attr"> image:</span> <span class="string">portainer/portainer</span></span><br><span class="line"><span class="attr"> command:</span> <span class="bullet">-H</span> <span class="attr">tcp://tasks.agent:9001</span> <span class="bullet">--tlsskipverify</span></span><br><span class="line"><span class="attr"> volumes:</span></span><br><span class="line"><span class="attr"> - portainer-data:</span><span class="string">/data</span></span><br><span class="line"><span class="attr"> networks:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">private</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">public</span></span><br><span class="line"><span class="attr"> deploy:</span></span><br><span class="line"><span class="attr"> placement:</span></span><br><span class="line"><span class="attr"> constraints:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">node.role</span> <span class="string">==</span> <span class="string">manager</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.frontend.rule=Host:portainer.${DOMAIN}</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.enable=true</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.port=9000</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.tags=public</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.docker.network=public</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.redirectorservice.frontend.entryPoints=http</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.redirectorservice.frontend.redirect.entryPoint=https</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.webservice.frontend.entryPoints=https</span></span><br><span class="line"></span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line"><span class="attr"> private:</span></span><br><span class="line"><span class="attr"> driver:</span> <span class="string">overlay</span></span><br><span class="line"><span class="attr"> name:</span> <span class="string">private</span></span><br><span class="line"><span class="attr"> public:</span></span><br><span class="line"><span class="attr"> external:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line"><span class="attr"> portainer-data:</span> <span class="string">{}</span></span><br></pre></td></tr></table></figure>
<p>Om de stack** uit te rollen voeren we het volgende commando uit om een manager node:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker stack deploy -c docker-compose.portainer.yml portainer</span><br></pre></td></tr></table></figure></p>
<p>Vanaf nu kunnen we Portainer benaderen op de URL <a href="https://portainer.yourdomain.com" target="_blank" rel="noopener">https://portainer.yourdomain.com</a> en kunnen we hier vandaan de rest van de services[^1] en stacks* deployen en beheren.</p>
<p>Een kleine samenvatting wat er gebeurt in dit Docker-Compose[^3] bestand:</p>
<ul>
<li>We maken een container aan op basis van portainer/portainer en portainer/agent.</li>
<li>We publiceren poort 9000 voor de UI.</li>
<li>We regelen de Traefik configuratie in.</li>
<li>We mounten een volume voor de Portainer data.</li>
<li>We zorgen dat de Portainer container wordt uitgerold op een manager in het cluster en dat de agent op elke node in het cluster wordt uitgerold. </li>
<li>We maken een netwerk aan met de naam Private en koppelen dit aan de agent en de Portainer UI.</li>
<li>We koppelen de Portainer UI ook aan het netwerk Public wat we hebben aangemaakt in de Traefik deploy.</li>
</ul>
<p>Voor meer informatie over Portainer kijk op: <a href="https://www.portainer.io/" target="_blank" rel="noopener">https://www.portainer.io/</a>. </p>
<p>Omdat ik Portainer gebruik als beheer tool kan ik het laatste punt afvinken van mijn lijstje:</p>
<ul>
<li>Het beheren van de containers moet gemakkelijk en overzichtelijk zijn. </li>
</ul>
<h2 id="De-combinatie-van-Docker-Swarm-Traefik-Let’s-Encrypt-en-Portainer"><a href="#De-combinatie-van-Docker-Swarm-Traefik-Let’s-Encrypt-en-Portainer" class="headerlink" title="De combinatie van Docker Swarm, Traefik, Let’s Encrypt en Portainer"></a>De combinatie van Docker Swarm, Traefik, Let’s Encrypt en Portainer</h2><p>We hebben het hierboven gehad over Docker Swarm, Traefik, Let’s Encrypt en Portainer maar hoe ziet dat landschap er nu uit.<br>In de volgende afbeelding heb ik een overzicht van het landschap zoals hierboven omschreven.</p>
<p><img src="/images/Docker-Swarm-landschap.png"></p>
<p>Ik heb gekozen om Azure Traffic Manager te gebruiken als loadbalancer voor om het verkeer te verdelen tussen de 3 verschillende managers. De gedachte hier achter is grotendeels dat het een erg goedkope service is in Azure en het werkt ook met externe endpoints. Je bent dus niet verplicht om je virtuele machines op Azure te moeten draaien dit geeft je weer de flexibiliteit om de machines overal te hosten.</p>
<p>Ik heb 3 endpoints gedefineerd in de Azure Traffic Manager 1 endpoint voor elke manager.<br>Ik heb Azure Traffic Manager ingeregeld dat hij voor de beste performance kiest. Je kunt een protocol, poort en eventueel een pad opgeven wat hij moet controleren. Mocht het endpoint niet meer beschikbaar zijn zal er geen verkeer meer naar toe gestuurd worden. Dus als er een storing of een update is van een manager zal er geen downtime zijn in de applicaties. Als er een manager niet bereikbaar is hebben we nog 2 andere managers over welke het werk overnemen.</p>
<p><img src="/images/Docker-Swarm-traffic-manager-configuration.png"></p>
<p>Hierna komt het verkeer binnen op de Traefik loadbalancer welke de Reverse proxy en de SSL-certificaten verzorgd. Traefik weet welk request er naar welke container gestuurd moet worden door middel van de labels welke zijn ingesteld bij het deployen van de stack[^2] of service[^1] en Docker maakt intern gebruikt van zijn eigen DNS server zodat er bekend is welke container er op welke node draait. </p>
<p>Zie hier een voorbeeld Docker-Compose[^3] file met labels om een Docker container met een webapplicatie te deployen als stack** op een Docker Swarm cluster.<br><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr"> version:</span> <span class="string">'3.7'</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"><span class="attr"> web:</span></span><br><span class="line"><span class="attr"> image:</span> <span class="string">marcoippel/web:0.1</span></span><br><span class="line"><span class="attr"> environment:</span> </span><br><span class="line"><span class="bullet"> -</span> <span class="string">ASPNETCORE_URLS=http://+:5000</span></span><br><span class="line"><span class="attr"> networks:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">public</span></span><br><span class="line"><span class="attr"> deploy:</span></span><br><span class="line"><span class="attr"> placement:</span></span><br><span class="line"><span class="attr"> constraints:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">node.role</span> <span class="string">==</span> <span class="string">worker</span></span><br><span class="line"><span class="attr"> labels:</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.frontend.rule=Host:test.${DOMAIN}</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.enable=true</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.port=80</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.tags=public</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.docker.network=public</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.redirectorservice.frontend.entryPoints=http</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.redirectorservice.frontend.redirect.entryPoint=https</span></span><br><span class="line"><span class="bullet"> -</span> <span class="string">traefik.webservice.frontend.entryPoints=https</span></span><br><span class="line"></span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line"><span class="attr"> public:</span></span><br><span class="line"><span class="attr"> external:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure></p>
<p>Het Docker-Compose[^3] bestand kan in Portainer als stack gedeployed worden op het cluster.</p>
<h2 id="Conclusie"><a href="#Conclusie" class="headerlink" title="Conclusie"></a>Conclusie</h2><p>Ik vind dat de combinatie van de verschillende tools een goede basis is voor het applicatielandschap. Het is flexibel, schaalbaar en niet bijzonder complex. Docker Swarm staat ook bekend om zijn eenvoudigheid. Aangezien ik geen infrastructuur met honderden nodes en duizenden containers hoef te hosten is dit een hele geschikte oplossing. Docker Swarm heeft niet een hele steile leercurve dus je kunt er al snel mee aan de slag. Door de reverse proxy van Traefik kunnen virtuele hosts automatisch worden aangemaakt en is SSL meteen geregeld. Met Portainer als UI voor het beheer kun je in een mum van tijd een heel applicatie landschap optuigen zonder al te veel tijd en kosten te moeten investeren.</p>
<p>Om het laatste puntje van mijn lijstje af te kunnen vinken heb ik hier een klein overzicht met een kosten indicatie van 3 verschillende providers waar je een docker Swarm cluster kan hosten. De totaal kosten welke hieronder staan zijn gebaseerd op 3 manager nodes en 2 worker nodes. De Manager nodes hebben allemaal 2 gig geheugen en de workers hebben 8 gig geheugen.</p>
<table>
<thead>
<tr>
<th style="text-align:left">Provider</th>
<th style="text-align:left">Manager VM</th>
<th style="text-align:left">Worker VM</th>
<th style="text-align:left">Totaal</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left"><a href="https://azure.microsoft.com/nl-nl/pricing/details/virtual-machines/ubuntu-advantage-standard/" target="_blank" rel="noopener">Azure</a></td>
<td style="text-align:left">B1MS €32/mo</td>
<td style="text-align:left">B2MS €76/mo</td>
<td style="text-align:left">€248/mo</td>
</tr>
<tr>
<td style="text-align:left"><a href="https://www.digitalocean.com/pricing/#Compute" target="_blank" rel="noopener">Digital Ocean</a></td>
<td style="text-align:left">Standard $10/mo</td>
<td style="text-align:left">Standard $40/mo</td>
<td style="text-align:left">$110/mo</td>
</tr>
<tr>
<td style="text-align:left"><a href="https://www.strato.nl/server/vps-linux/" target="_blank" rel="noopener">Strato</a></td>
<td style="text-align:left">Linux V10 €5/mo</td>
<td style="text-align:left">Linux V30 €15/mo</td>
<td style="text-align:left">€45/mo</td>
</tr>
</tbody>
</table>
<p>De kosten voor de Azure Traffic Manager zijn: Eerste 1 miljard DNS-query’s/maand €0,456 per miljoen query’s. <a href="https://azure.microsoft.com/nl-nl/pricing/details/traffic-manager/" target="_blank" rel="noopener">https://azure.microsoft.com/nl-nl/pricing/details/traffic-manager/</a></p>
<p>Zoals je ziet in het overzicht 3 providers waarvan Digital Ocean en Azure echt serieuze cloud providers zijn met veel meer services dan alleen virtuele machines. Azure biedt een uptime van 99,9% en Digital Ocean een uptime van 99,99% dit is ook iets waar je voor betaald. Zo zie je dat voor iedereen zijn portemonnee een oplossing is. </p>
<p>Mocht je toch niet tevreden zijn met de service van je hosting provider dan is het heel gemakkelijk om je infrastructuur op te pakken en deze gewoon bij een andere provider te hosten. Het zijn namelijk gewoon container images en yaml files voor de configuratie en je bent in een mum van tijd weer up en running. </p>
<p>[^1]: Een service is een image van een microservice in de context van een grotere toepassing.<br>[^2]: Een stack is een Docker-compose file met services gedefinieerd welke in een keer uitgerold kan worden.<br>[^3]: Docker-Compose is een hulpmiddel voor het definiëren en uitvoeren van Docker-toepassingen met meerdere containers.</p>
</div>
<footer class="article-footer">
<a data-url="https://cloudrepublic.github.io/2019/11/03/Docker-Swarm/" data-id="ck3k8b2rp00016cunizflfpno" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Docker-swarm/" rel="tag">Docker swarm</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Let-s-Encrypt/" rel="tag">Let's Encrypt</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Portainer/" rel="tag">Portainer</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Traefik/" rel="tag">Traefik</a></li></ul>
</footer>
</div>
</article>
<article id="post-CICD-SharePointFramework-part-1" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2019/10/17/CICD-SharePointFramework-part-1/" class="article-date">
<time datetime="2019-10-17T10:15:31.000Z" itemprop="datePublished">2019-10-17</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2019/10/17/CICD-SharePointFramework-part-1/">Continuous Integration & Deployment with SharePoint Framework Solutions - Part 1 of 2</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>Microsoft heeft goed werk verricht voor ons als SPFx developers om met <em>gulp serve</em> snel te kunnen ontwikkelen binnen SharePoint Online. Je kunt lokaal je webpart testen en zelfs verbinden met live data binnen SharePoint Online zonder dat het beschikbaar is voor anderen.</p>
<p>Als SPFx developer leveren we uiteindelijk ons SharePoint Online maatwerk aan onze klant. We zijn dan gewend de commando’s als <em>gulp bundle -ship</em> en <em>gulp package-solution -ship</em> uit te voeren en te uploaden naar onze App Catalog en te “Implementeren”. Maar hiervoor heb je rechten nodig en niet altijd ben je daarvoor bevoegd. Deze verantwoording is vaak belegd bij de applicatiebeheerder die verantwoordelijk is voor een stabiele applicatie.</p>
<p>Hoe fijn zou het zijn als je dit samen met de applicatiebeheerder kunt inregelen via Azure DevOps Continuous Integration en Deployment. Bij de Collaboration Summit in Wiesbaden in mei 2019 werd gedemonstreerd hoe dit ingeregeld kan worden. In deze blog ga ik dit met mijn Visual Studio Enterprise subscription in mijn Office 365 Developer tenant inrichten. Het doel van deel 1 van deze blog is wanneer dingen gecommit worden in de master-branch, dat die code gebuild wordt en de artifact ‘myproject.sppkg’ te downloaden is. In de volgende blog (Part 2) gaan we de installatie van deze package automatiseren met Continuous Deployment.</p>
<p><strong>Azure DevOps - Voorbereiding</strong><br>Voordat we de pipelines gaan inrichten, hebben we de volgende uitgangspunten:</p>
<ul>
<li>Broncode van een SPFx-webpart solution staat in een Git-repository in Azure DevOps.</li>
<li>De SPFx-webpart solution is buildable zonder fouten.</li>
<li>Je heb rechten binnen AzureDevOps om een Build-pipeline in te richten.</li>
</ul>
<p><strong>Azure DevOps - Maken van de Build-pipeline voor een SharePoint Framework project</strong><br>Er zijn verschillende manieren om een build-pipeline op te zetten. Als je voor het eerst begint met build-pipelines, dan is de simpelste manier om de ‘classic editor’ te gebruiken en hiermee te experimenteren. De definitie en alle wijzigingen die je op de build pipeline maakt staan los van de source code.<br>De andere manier is d.m.v. een YAML-file die wel naast de source code staat en dus mee moet komen in je commit. In deze blog maken we een SharePoint Framework Build pipeline dmv een YAML-file:</p>
<ul>
<li>Ga naar je Azure DevOps project en ga naar ‘Pipelines’ -> ‘Build’.</li>
<li>Maak een nieuwe Build pipeline aan.<br><img src="/images/cicd-sharepointframework/01-newbuildpipeline.png"></li>
<li>Kies vervolgens bij ‘Where is your code?’ voor ‘Azure Repos Git’ als je code standaard in Azure DevOps staat met Git. </li>
<li>Kies vervolgens je Repository.</li>
<li><p>Kies vervolgens bij ‘Configure your pipeline’ voor de optie ‘Starter pipeline’.<br><img src="/images/cicd-sharepointframework/02-yaml-initialcode.png"><br><em>Figuur 1: Initiële yaml-code</em></p>
</li>
<li><p>Vervang de initiële code met het volgende:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"># Deze build start wanneer er een wijziging op de 'master'-branch wordt gedaan.</span><br><span class="line">trigger:</span><br><span class="line">- master</span><br><span class="line"></span><br><span class="line"># We gebruiken een hosted VM met Visual Studio 2017 op een Windows 2016 server van Azure</span><br><span class="line">pool:</span><br><span class="line"> vmImage: 'vs2017-win2016'</span><br><span class="line"> </span><br><span class="line">steps:</span><br><span class="line"># Installeer Node 8.x</span><br><span class="line">- task: UseNode@1</span><br><span class="line"> displayName: 'Install Node 8.x'</span><br><span class="line"> inputs:</span><br><span class="line"> version: '8.x'</span><br><span class="line"># Installeer alle packages van het project met Node</span><br><span class="line">- task: Npm@1</span><br><span class="line"> displayName: 'Run ''npm install'''</span><br><span class="line"> inputs:</span><br><span class="line"> command: 'install'</span><br><span class="line"># Gulp-commando: Verzamelen van alle broncode van het SharePoint Framework-project</span><br><span class="line">- task: Gulp@1</span><br><span class="line"> displayName: 'Run ''gulp bundle --ship'''</span><br><span class="line"> inputs:</span><br><span class="line"> gulpFile: 'gulpfile.js'</span><br><span class="line"> targets: 'bundle'</span><br><span class="line"> arguments: '--ship'</span><br><span class="line"> enableCodeCoverage: false</span><br><span class="line"># Gulp-commando: Maak een solution package van het SharePoint Framework-project</span><br><span class="line">- task: Gulp@1</span><br><span class="line"> displayName: 'Run ''gulp package-solution --ship'''</span><br><span class="line"> inputs:</span><br><span class="line"> gulpFile: 'gulpfile.js'</span><br><span class="line"> targets: 'package-solution'</span><br><span class="line"> arguments: '--ship'</span><br><span class="line"> enableCodeCoverage: false</span><br><span class="line"># Publiceer de SharePoint Solution Package onder de artifact naam 'SPFx-myproject'</span><br><span class="line">- task: PublishPipelineArtifact@1</span><br><span class="line"> displayName: 'Publish ''MyProject'' to artifact ''SPFx-myproject'''</span><br><span class="line"> inputs:</span><br><span class="line"> targetPath: 'sharepoint/solution/myproject-webpart.sppkg'</span><br><span class="line"> artifact: 'SPFx-myproject'</span><br></pre></td></tr></table></figure>
</li>
</ul>
<p><em>Figuur 2: Standaard yaml build pipeline voor ‘myproject’</em></p>
<ul>
<li>Pas indien nodig alle ‘myproject’ teksten aan naar wens.</li>
<li>Klik op ‘Save & Run’<br><img src="/images/cicd-sharepointframework/03-yaml-save-and-run.png"><br><em>Figuur 3: Save and run build pipeline</em></li>
</ul>
<p>Je ziet hier dat je je wijziging direct in de master-branch kan aanbrengen of apart in een Pull Request in een nieuwe branch. Voor nu kiezen we voor ‘Commit directly to the master branch’.</p>
<ul>
<li>De build gaat van start:<br><img src="/images/cicd-sharepointframework/04-build-running.png"><br><em>Figuur 4: Azure built nu MyProject-artifact</em></li>
<li>Na enkele minuten is de build klaar en verschijnt rechtsboven in het scherm de knop ‘Artifacts’.<br>Onder deze knop kan de package gedownload worden.<br><img src="/images/cicd-sharepointframework/05-build-finished.png"><br><em>Figuur 5: Artifact is gebuild en te downloaden</em></li>
</ul>
<p>In de volgende blog gaan we deze package installeren via Continuous Deployment.</p>
</div>
<footer class="article-footer">
<a data-url="https://cloudrepublic.github.io/2019/10/17/CICD-SharePointFramework-part-1/" data-id="ck3k8b2rg00006cuns0zw55wo" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Continuous-Deployment/" rel="tag">Continuous Deployment</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Continuous-Integration/" rel="tag">Continuous Integration</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Frontend/" rel="tag">Frontend</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Office-365/" rel="tag">Office 365</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/SPFx/" rel="tag">SPFx</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/SharePoint-Framework/" rel="tag">SharePoint Framework</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/SharePoint-Online/" rel="tag">SharePoint Online</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Webpart/" rel="tag">Webpart</a></li></ul>
</footer>
</div>
</article>
<article id="post-Proof-of-concept-met-azure-batch" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2019/06/27/Proof-of-concept-met-azure-batch/" class="article-date">
<time datetime="2019-06-27T17:54:37.000Z" itemprop="datePublished">2019-06-27</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2019/06/27/Proof-of-concept-met-azure-batch/">Proof of concept met Azure Batch</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<h2 id="Case-omschrijving"><a href="#Case-omschrijving" class="headerlink" title="Case omschrijving "></a>Case omschrijving </h2><p>Net als in mijn vorige <a href="https://cloudrepublic.github.io/2019/03/28/Proof-of-concept-met-durable-functions/">blog post</a> heb ik een Proof of Concept gemaakt om grote hoeveelheden XML bestanden te transformeren, het gaat dan 3000 bestanden per keer met een totale grote van 18 gigabyte. Voor dit artikel kan ik wegens privacy redenen niet de echte data gebruiken en heb ik een dataset gebruikt van <a href="https://www.kaggle.com/datasets" target="_blank" rel="noopener">https://www.kaggle.com/datasets</a>. Het gaat hier om een dataset van landen en de wijnen.</p>
<p>Het doel is om de wijnen uit de XML dump te halen en deze om te zetten naar een JSON formaat en deze bestanden te uploaden in een blob container. We krijgen dus per wijn een JSON bestand in een blob container. Dit alles moet gebeuren op basis Azure Batch met een Azure Function als orchestrator.</p>
<h2 id="Wat-is-de-opzet-van-de-POC"><a href="#Wat-is-de-opzet-van-de-POC" class="headerlink" title="Wat is de opzet van de POC"></a>Wat is de opzet van de POC</h2><p>We gaan de XML bestanden transformeren doormiddel van een Azure Batch component en we starten en beheren de Azure Batch doormiddel van een Azure Function.<br>De Azure Function zal de XML bestanden toevoegen aan een Job in Azure Batch doormiddel van een <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-event-grid" target="_blank" rel="noopener">event grid trigger</a> en via een httptrigger is de voortgang te zien van het batch proces. </p>
<p><img src="/images/azure-batch-overview.png"></p>
<h2 id="Wat-is-Azure-Batch"><a href="#Wat-is-Azure-Batch" class="headerlink" title="Wat is Azure Batch"></a>Wat is Azure Batch</h2><p>Azure Batch is plat gezegd eigenlijk een beheer tool voor virtuele machines. Elk van deze machines kan een taak oppakken en dit als input gebruiken voor een commandline applicatie en het resultaat uploaden in bijvoorbeeld een blob container. </p>
<p>Voor de complete omschrijving wat je met Azure Batch kunt doen verwijs ik je graag door naar de Microsoft site <a href="https://azure.microsoft.com/nl-nl/services/batch/" target="_blank" rel="noopener">https://azure.microsoft.com/nl-nl/services/batch/</a></p>
<h2 id="Hoe-werkt-het-Proof-of-Concept"><a href="#Hoe-werkt-het-Proof-of-Concept" class="headerlink" title="Hoe werkt het Proof of Concept"></a>Hoe werkt het Proof of Concept</h2><p>Azure Batch bestaat uit een aantal componenten.</p>
<ul>
<li>Een Task is een opdracht welke uitgevoerd dient te worden op een node. </li>
<li>Een Job is een verzameling van tasks. Aan een Job hangt ook een Pool.</li>
<li>Een Pool is een verzameling van nodes.</li>
<li>Een Node is een virtuele machine welke een van de tasks gaat uitvoeren. </li>
</ul>
<p>Ik heb 40 bestanden met landen en wijnen. 1 bestand is ongeveer 75 mb groot. Voor testdoeleinden zijn dit dezelfde bestanden met een andere naam. Dit is meer om een gelijkwaardige load op de functie te krijgen als bij de echte POC.</p>
<p>Ik heb een Azure Function welke een event grid trigger heeft welke afgaat op het moment dat er een bestand wordt geupload in de blobcontainer.<br><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">[<span class="meta">FunctionName(<span class="meta-string">"BatchOrchestrator"</span>)</span>]</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">Run</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> [EventGridTrigger] EventGridEvent eventGridEvent,</span></span></span><br><span class="line"><span class="function"><span class="params"> ExecutionContext context,</span></span></span><br><span class="line"><span class="function"><span class="params"> ILogger log</span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">var</span> data = JsonConvert.DeserializeObject<StorageBlobCreatedEventData>(eventGridEvent.Data.ToString());</span><br><span class="line"> <span class="keyword">string</span> name = data.Url.Split(<span class="string">'/'</span>).Last();</span><br><span class="line"></span><br><span class="line"> log.LogInformation(<span class="string">$"C# Blob trigger function Processed blob\n Name:<span class="subst">{name}</span>"</span>);</span><br><span class="line"> <span class="keyword">await</span> RunBatch(log, context, name);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>De bestandsnaam wordt uit de URL gehaald en doorgestuurd naar de methode RunBatch.<br>De methode RunBatch initialiseert een BatchClient met de credentials welke opgegeven zij in de config.<br><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">RunBatch</span>(<span class="params">ILogger log, ExecutionContext context, <span class="keyword">string</span> name</span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">var</span> config = ReadSettings(context);</span><br><span class="line"></span><br><span class="line"> BatchSharedKeyCredentials credentials = <span class="keyword">new</span> BatchSharedKeyCredentials(config[<span class="string">"BatchUrl"</span>], config[<span class="string">"BatchAccount"</span>], config[<span class="string">"BatchKey"</span>]);</span><br><span class="line"> <span class="keyword">using</span> (BatchClient batchClient = BatchClient.Open(credentials))</span><br><span class="line"> {</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>Hierna gaan we kijken of er al een job bestaat in het Azure Batch Account. Als er al een job bestaat en hij is nog actief of wordt aangemaakt voegen we hier een task aan toe. Als er geen job actief is of wordt opgestart dan maken we een Job aan. Dit gebeurt in de <em>CreateJob</em> methode.<br><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">RunBatch</span>(<span class="params">ILogger log, ExecutionContext context, <span class="keyword">string</span> name</span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> config = ReadSettings(context);</span><br><span class="line"></span><br><span class="line"> BatchSharedKeyCredentials credentials = <span class="keyword">new</span> BatchSharedKeyCredentials(config[<span class="string">"BatchUrl"</span>], config[<span class="string">"BatchAccount"</span>], config[<span class="string">"BatchKey"</span>]);</span><br><span class="line"> <span class="keyword">using</span> (BatchClient batchClient = BatchClient.Open(credentials))</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">string</span> jobId = <span class="keyword">string</span>.Empty;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span></span><br><span class="line"> {</span><br><span class="line"> batchClient.CustomBehaviors.Add(RetryPolicyProvider.ExponentialRetryProvider(TimeSpan.FromSeconds(<span class="number">5</span>), <span class="number">3</span>));</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (batchClient.JobOperations.ListJobs().Any())</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">var</span> jobs = batchClient.JobOperations.ListJobs();</span><br><span class="line"> CloudJob activeJob = jobs.FirstOrDefault(job => job.State == JobState.Active || job.State == JobState.Enabling);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (activeJob != <span class="literal">null</span>)</span><br><span class="line"> {</span><br><span class="line"> log.LogDebug(<span class="string">"Job still active"</span>);</span><br><span class="line"> CreateTaskIfNotExists(batchClient, activeJob.Id, name);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> jobId = <span class="keyword">await</span> CreateJob(name, batchClient);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> jobId = <span class="keyword">await</span> CreateJob(name, batchClient);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">catch</span> (Exception e)</span><br><span class="line"> {</span><br><span class="line"> log.LogError(e, e.Message);</span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">string</span>.IsNullOrEmpty(jobId))</span><br><span class="line"> {</span><br><span class="line"> log.LogDebug(<span class="string">$"Deleting job: <span class="subst">{jobId}</span>"</span>);</span><br><span class="line"> <span class="keyword">await</span> batchClient.JobOperations.DeleteJobAsync(jobId);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>Een Job heeft een pool met nodes nodig welke het werk uitvoeren. We gaan deze dus eerst aanmaken.</p>
<ul>
<li>We specificeren het besturingssysteem in dit geval staat de waarde <em>5</em> voor <em>Windows Server 2016</em> voor de overige waardes check de Azure Guest OS Releases <a href="https://docs.microsoft.com/en-us/azure/cloud-services/cloud-services-guestos-update-matrix#releases" target="_blank" rel="noopener">https://docs.microsoft.com/en-us/azure/cloud-services/cloud-services-guestos-update-matrix#releases</a></li>
<li>We specificeren een virtuele machine size in dit geval een <em>standard_d1_v2</em> voor de overige ondersteunde machines check <a href="https://docs.microsoft.com/en-us/azure/batch/batch-pool-vm-sizes#supported-vm-families-and-sizes" target="_blank" rel="noopener">https://docs.microsoft.com/en-us/azure/batch/batch-pool-vm-sizes#supported-vm-families-and-sizes</a></li>
<li>We specificeren hoeveel tasks er per node gedraaid mogen worden in dit geval 4. Er zullen dus 4 tasks per keer op de node worden gestart.</li>
<li>We specificeren welke applicatie er op de node gedraaid dient te worden. Dit moet een applicatie of script zijn welke via de commandline te draaien is. Deze applicatie kan als een zip bestand worden gupload in de portal. </li>
<li>We zetten de lifetime op <em>PoolLifetimeOption.Job</em> dit wil zeggen zodra alle tasks in de Job klaar zijn zal de pool verwijdert worden en zul je dus ook niet meer betalen voor de virtuele machines. </li>
</ul>
<figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> PoolInformation <span class="title">CreatePool</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> PoolInformation()</span><br><span class="line"> {</span><br><span class="line"> AutoPoolSpecification = <span class="keyword">new</span> AutoPoolSpecification()</span><br><span class="line"> {</span><br><span class="line"> AutoPoolIdPrefix = <span class="string">"Wine"</span>,</span><br><span class="line"> PoolSpecification = <span class="keyword">new</span> PoolSpecification()</span><br><span class="line"> {</span><br><span class="line"> CloudServiceConfiguration = <span class="keyword">new</span> CloudServiceConfiguration(<span class="string">"5"</span>),</span><br><span class="line"> VirtualMachineSize = <span class="string">"standard_d1_v2"</span>,</span><br><span class="line"> MaxTasksPerComputeNode = <span class="number">4</span>,</span><br><span class="line"> ApplicationPackageReferences = <span class="keyword">new</span> List<ApplicationPackageReference>()</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">new</span> ApplicationPackageReference()</span><br><span class="line"> {</span><br><span class="line"> ApplicationId = <span class="string">"converter"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> KeepAlive = <span class="literal">false</span>,</span><br><span class="line"> PoolLifetimeOption = PoolLifetimeOption.Job</span><br><span class="line"> }</span><br><span class="line"> };</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>Nu we een Pool hebben kunnen we deze koppelen aan de Job. De <em>CreateJob</em> methode maakt een unieke naam aan voor de job doormiddel van een timestamp te prefixen met “WineConverter”.</p>
<figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">async</span> Task<<span class="keyword">string</span>> <span class="title">CreateJob</span>(<span class="params"><span class="keyword">string</span> name, BatchClient batchClient</span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">string</span> jobId;</span><br><span class="line"></span><br><span class="line"> CloudJob activeJob;</span><br><span class="line"> jobId = CreateJobId(<span class="string">"WineConverter"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//create pool</span></span><br><span class="line"> PoolInformation pool = CreatePool();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// create a job</span></span><br><span class="line"> activeJob = <span class="keyword">await</span> CreateJobAsync(batchClient, jobId, pool);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//create tasks from the blobs</span></span><br><span class="line"> CreateTaskIfNotExists(batchClient, jobId, name);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> activeJob.RefreshAsync();</span><br><span class="line"> activeJob.OnAllTasksComplete = OnAllTasksComplete.TerminateJob;</span><br><span class="line"> <span class="keyword">await</span> activeJob.CommitChangesAsync();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> jobId;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>We controlleren eerst of de task al is aangemaakt en is toegevoegd aan de job, zoniet dan voegen we hem toe. De naam van de task mag alleen letters en cijfers bevatten en een koppelteken en underscore.</p>
<p>Ook stellen we in welke package de task moet starten met welke argumenten. Hier starten we converter.exe met als argument een naam van de blob (in dit geval een XML bestand met wijn data).</p>
<p>De converter.exe bevat alle logica om de xml te verwerken en het resultaat te uploaden in een Azure blob container.<br>In de Azure portal kan je een package uploaden welke op de nodes geinstalleerd moeten worden. Meer informatie over hoe packages werken met Azure batch is te vinden op: <a href="https://docs.microsoft.com/en-us/azure/batch/batch-application-packages" target="_blank" rel="noopener">https://docs.microsoft.com/en-us/azure/batch/batch-application-packages</a></p>
<figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">CreateTaskIfNotExists</span>(<span class="params">BatchClient batchClient, <span class="keyword">string</span> jobId, <span class="keyword">string</span> blobName</span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> IPagedEnumerable<CloudTask> tasks = batchClient.JobOperations.ListTasks(jobId);</span><br><span class="line"> <span class="keyword">string</span> taskId = <span class="string">$"task_<span class="subst">{SanitizeString(blobName)}</span>"</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (tasks.Any(x => x.Id == taskId))</span><br><span class="line"> {</span><br><span class="line"> Console.WriteLine(<span class="string">$"Task with id: <span class="subst">{taskId}</span> all ready exists"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> batchClient.JobOperations.AddTask(jobId, <span class="keyword">new</span> CloudTask(taskId, <span class="string">$"cmd /c %AZ_BATCH_APP_PACKAGE_CONVERTER%\\converter.exe -name <span class="subst">{blobName}</span>"</span>));</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h2 id="De-volledige-code"><a href="#De-volledige-code" class="headerlink" title="De volledige code"></a>De volledige code</h2><p>Zie hieronder de volledige code. Er is een extra function aan toegevoegd met een httptrigger zodra je deze aanroept worden alle jobs met de bijhorende tasks weergegeven en de status van de tasks. Het endpoint is nu niet beveiligd maar dit is omdat het een demo is.</p>
<figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System;</span><br><span class="line"><span class="keyword">using</span> System.Collections.Generic;</span><br><span class="line"><span class="keyword">using</span> System.IO;</span><br><span class="line"><span class="keyword">using</span> System.Linq;</span><br><span class="line"><span class="keyword">using</span> System.Net.Http;</span><br><span class="line"><span class="keyword">using</span> System.Threading.Tasks;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Azure.Batch;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Azure.Batch.Auth;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Azure.Batch.Common;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Azure.WebJobs;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Azure.WebJobs.Extensions.Http;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Configuration;</span><br><span class="line"><span class="keyword">using</span> Microsoft.Extensions.Logging;</span><br><span class="line"><span class="keyword">using</span> Newtonsoft.Json;</span><br><span class="line"><span class="keyword">using</span> JobState = Microsoft.Azure.Batch.Common.JobState;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">WineConverter</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title">BatchOrchestrator</span></span><br><span class="line"> {</span><br><span class="line"> [<span class="meta">FunctionName(<span class="meta-string">"BatchOrchestrator"</span>)</span>]</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">Run</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> [EventGridTrigger] EventGridEvent eventGridEvent,</span></span></span><br><span class="line"><span class="function"><span class="params"> ExecutionContext context,</span></span></span><br><span class="line"><span class="function"><span class="params"> ILogger log</span>)</span></span><br><span class="line"><span class="function"></span> {</span><br><span class="line"> <span class="keyword">var</span> data = JsonConvert.DeserializeObject<StorageBlobCreatedEventData>(eventGridEvent.Data.ToString());</span><br><span class="line"> <span class="keyword">string</span> name = data.Url.Split(<span class="string">'/'</span>).Last();</span><br><span class="line"></span><br><span class="line"> log.LogInformation(<span class="string">$"C# Blob trigger function Processed blob\n Name:<span class="subst">{name}</span>"</span>);</span><br><span class="line"> <span class="keyword">await</span> RunBatch(log, context, name);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">RunBatch</span>(<span class="params">ILogger log, ExecutionContext context, <span class="keyword">string</span> name</span>)</span></span><br><span class="line"><span class="function"></span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> config = ReadSettings(context);</span><br><span class="line"></span><br><span class="line"> BatchSharedKeyCredentials credentials = <span class="keyword">new</span> BatchSharedKeyCredentials(config[<span class="string">"BatchUrl"</span>], config[<span class="string">"BatchAccount"</span>], config[<span class="string">"BatchKey"</span>]);</span><br><span class="line"> <span class="keyword">using</span> (BatchClient batchClient = BatchClient.Open(credentials))</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">string</span> jobId = <span class="keyword">string</span>.Empty;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span></span><br><span class="line"> {</span><br><span class="line"> batchClient.CustomBehaviors.Add(RetryPolicyProvider.ExponentialRetryProvider(TimeSpan.FromSeconds(<span class="number">5</span>), <span class="number">3</span>));</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (batchClient.JobOperations.ListJobs().Any())</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">var</span> jobs = batchClient.JobOperations.ListJobs();</span><br><span class="line"> CloudJob activeJob = jobs.FirstOrDefault(job => job.State == JobState.Active || job.State == JobState.Enabling);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (activeJob != <span class="literal">null</span>)</span><br><span class="line"> {</span><br><span class="line"> log.LogDebug(<span class="string">"Job still active"</span>);</span><br><span class="line"> CreateTaskIfNotExists(batchClient, activeJob.Id, name);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> jobId = <span class="keyword">await</span> CreateJob(name, batchClient);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> jobId = <span class="keyword">await</span> CreateJob(name, batchClient);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">catch</span> (Exception e)</span><br><span class="line"> {</span><br><span class="line"> log.LogError(e, e.Message);</span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">string</span>.IsNullOrEmpty(jobId))</span><br><span class="line"> {</span><br><span class="line"> log.LogDebug(<span class="string">$"Deleting job: <span class="subst">{jobId}</span>"</span>);</span><br><span class="line"> <span class="keyword">await</span> batchClient.JobOperations.DeleteJobAsync(jobId);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> [<span class="meta">FunctionName(<span class="meta-string">"BatchStatus"</span>)</span>]</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">string</span> <span class="title">BatchStatus</span>(<span class="params">[HttpTrigger(AuthorizationLevel.Anonymous, <span class="string">"get"</span></span>)]HttpRequestMessage req, ExecutionContext context, ILogger log)</span></span><br><span class="line"><span class="function"></span> {</span><br><span class="line"> List<Job> jobList = <span class="keyword">new</span> List<Job>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> config = ReadSettings(context);</span><br><span class="line"> BatchSharedKeyCredentials credentials = <span class="keyword">new</span> BatchSharedKeyCredentials(config[<span class="string">"BatchUrl"</span>], config[<span class="string">"BatchAccount"</span>], config[<span class="string">"BatchKey"</span>]);</span><br><span class="line"> <span class="keyword">using</span> (BatchClient batchClient = BatchClient.Open(credentials))</span><br><span class="line"> {</span><br><span class="line"> IPagedEnumerable<CloudJob> jobs = batchClient.JobOperations.ListJobs();</span><br><span class="line"> <span class="keyword">if</span> (jobs.Any())</span><br><span class="line"> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">foreach</span> (CloudJob cloudJob <span class="keyword">in</span> jobs.ToList())</span><br><span class="line"> {</span><br><span class="line"> jobList.Add(<span class="keyword">new</span> Job()</span><br><span class="line"> {</span><br><span class="line"> Id = cloudJob.Id,</span><br><span class="line"> Name = cloudJob.DisplayName,</span><br><span class="line"> Status = cloudJob.State.ToString(),</span><br><span class="line"> Tasks = cloudJob.ListTasks().Select(task => <span class="keyword">new</span> JobTask()</span><br><span class="line"> {</span><br><span class="line"> Id = task.Id,</span><br><span class="line"> Name = task.DisplayName,</span><br><span class="line"> Status = task.State.ToString()</span><br><span class="line"> }).ToList()</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> JsonConvert.SerializeObject(jobList, Formatting.Indented);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">async</span> Task<<span class="keyword">string</span>> <span class="title">CreateJob</span>(<span class="params"><span class="keyword">string</span> name, BatchClient batchClient</span>)</span></span><br><span class="line"><span class="function"></span> {</span><br><span class="line"> <span class="keyword">string</span> jobId;</span><br><span class="line"></span><br><span class="line"> CloudJob activeJob;</span><br><span class="line"> jobId = CreateJobId(<span class="string">"WineConverter"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//create pool</span></span><br><span class="line"> PoolInformation pool = CreatePool();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// create a job</span></span><br><span class="line"> activeJob = <span class="keyword">await</span> CreateJobAsync(batchClient, jobId, pool);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//create tasks from the blobs</span></span><br><span class="line"> CreateTaskIfNotExists(batchClient, jobId, name);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> activeJob.RefreshAsync();</span><br><span class="line"> activeJob.OnAllTasksComplete = OnAllTasksComplete.TerminateJob;</span><br><span class="line"> <span class="keyword">await</span> activeJob.CommitChangesAsync();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> jobId;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> PoolInformation <span class="title">CreatePool</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function"></span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> PoolInformation()</span><br><span class="line"> {</span><br><span class="line"> AutoPoolSpecification = <span class="keyword">new</span> AutoPoolSpecification()</span><br><span class="line"> {</span><br><span class="line"> AutoPoolIdPrefix = <span class="string">"Wine"</span>,</span><br><span class="line"> PoolSpecification = <span class="keyword">new</span> PoolSpecification()</span><br><span class="line"> {</span><br><span class="line"> CloudServiceConfiguration = <span class="keyword">new</span> CloudServiceConfiguration(<span class="string">"5"</span>),</span><br><span class="line"> VirtualMachineSize = <span class="string">"standard_d1_v2"</span>,</span><br><span class="line"> MaxTasksPerComputeNode = <span class="number">4</span>,</span><br><span class="line"> ApplicationPackageReferences = <span class="keyword">new</span> List<ApplicationPackageReference>()</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">new</span> ApplicationPackageReference()</span><br><span class="line"> {</span><br><span class="line"> ApplicationId = <span class="string">"converter"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> KeepAlive = <span class="literal">false</span>,</span><br><span class="line"> PoolLifetimeOption = PoolLifetimeOption.Job</span><br><span class="line"> }</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">async</span> Task<CloudJob> <span class="title">CreateJobAsync</span>(<span class="params">BatchClient batchClient, <span class="keyword">string</span> jobId, PoolInformation pool</span>)</span></span><br><span class="line"><span class="function"></span> {</span><br><span class="line"> CloudJob unboundJob = batchClient.JobOperations.CreateJob();</span><br><span class="line"> unboundJob.Id = jobId;</span><br><span class="line"></span><br><span class="line"> unboundJob.PoolInformation = pool;</span><br><span class="line"> <span class="keyword">await</span> unboundJob.CommitAsync();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> unboundJob;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">CreateTaskIfNotExists</span>(<span class="params">BatchClient batchClient, <span class="keyword">string</span> jobId, <span class="keyword">string</span> blobName</span>)</span></span><br><span class="line"><span class="function"></span> {</span><br><span class="line"> IPagedEnumerable<CloudTask> tasks = batchClient.JobOperations.ListTasks(jobId);</span><br><span class="line"> <span class="keyword">string</span> taskId = <span class="string">$"task_<span class="subst">{SanitizeString(blobName)}</span>"</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (tasks.Any(x => x.Id == taskId))</span><br><span class="line"> {</span><br><span class="line"> Console.WriteLine(<span class="string">$"Task with id: <span class="subst">{taskId}</span> all ready exists"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> batchClient.JobOperations.AddTask(jobId, <span class="keyword">new</span> CloudTask(taskId, <span class="string">$"cmd /c %AZ_BATCH_APP_PACKAGE_CONVERTER%\\converter.exe -name <span class="subst">{blobName}</span>"</span>));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">string</span> <span class="title">CreateJobId</span>(<span class="params"><span class="keyword">string</span> prefix</span>)</span></span><br><span class="line"><span class="function"></span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">$"<span class="subst">{prefix}</span>-<span class="subst">{DateTime.Now:yyyyMMdd-HHmmss}</span>"</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">string</span> <span class="title">SanitizeString</span>(<span class="params"><span class="keyword">string</span> text</span>)</span></span><br><span class="line"><span class="function"></span> {</span><br><span class="line"> <span class="keyword">string</span> pattern = <span class="string">@"[^A-Za-z0-9-_]"</span>;</span><br><span class="line"> <span class="keyword">return</span> System.Text.RegularExpressions.Regex.Replace(text, pattern, <span class="keyword">string</span>.Empty);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> IConfigurationRoot <span class="title">ReadSettings</span>(<span class="params">ExecutionContext context</span>)</span></span><br><span class="line"><span class="function"></span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> ConfigurationBuilder()</span><br><span class="line"> .SetBasePath(context.FunctionAppDirectory)</span><br><span class="line"> .AddJsonFile(<span class="string">"local.settings.json"</span>, optional: <span class="literal">true</span>, reloadOnChange: <span class="literal">true</span>)</span><br><span class="line"> .AddEnvironmentVariables()</span><br><span class="line"> .Build();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">JobTask</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">string</span> Id { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">string</span> Name { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">string</span> Status { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Job</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">string</span> Id { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">string</span> Name { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">string</span> Status { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line"> <span class="keyword">public</span> List<JobTask> Tasks { <span class="keyword">get</span>; <span class="keyword">set</span>; }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h2 id="Conclusie"><a href="#Conclusie" class="headerlink" title="Conclusie"></a>Conclusie</h2><p>Azure batch is echt een serieuze keuze als je grote hoeveelheden data moet verwerken en je wilt in controle zijn wat er allemaal gebeurt.<br>Je kan zowel horizontaal als verticaal schalen en het aantal nodes wat het werk kan doen is standaard 20 maar je kunt een request doen voor meer nodes. Voor de recource limieten check de documentatie <a href="https://docs.microsoft.com/en-us/azure/batch/batch-quota-limit" target="_blank" rel="noopener">https://docs.microsoft.com/en-us/azure/batch/batch-quota-limit</a>.</p>
<p>Ik ken zelf weinig projecten waar ze Azure batch gebruiken maar ik ben echt onder de indruk hoe simpel en krachtig Azure batch is plus je betaald alleen voor de tijd dat de nodes ook echt iets doen dus geen vaste maandelijkse kosten.</p>
<p>Deze POC heeft het qua performance zijn doel wel behaald wat we voor ogen hadden alleen het bevat toch net te veel stappen om het volledig via CI/CD gemakkelijk te deployen. </p>
<p>In een volgende blog zal ik de uiteindelijke oplossing uitwerken met verschillende functie apps op een consumption plan welke aan alle eisen voldoet.</p>
</div>
<footer class="article-footer">
<a data-url="https://cloudrepublic.github.io/2019/06/27/Proof-of-concept-met-azure-batch/" data-id="cjxfqz4xm0000xgunr03325se" class="article-share-link">Share</a>
</footer>
</div>
</article>
<article id="post-Getting-started-with-SPfx" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2019/06/14/Getting-started-with-SPfx/" class="article-date">
<time datetime="2019-06-14T14:15:35.000Z" itemprop="datePublished">2019-06-14</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2019/06/14/Getting-started-with-SPfx/">Getting started with SharePoint Framework</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>Voor een klant zijn we begonnen met het bouwen van hun eerste SharePoint Online webpart. Binnen een Citrix omgeving waar we een Developer VM ter beschikking hebben, moeten we eerst een ontwikkelomgeving opzetten. Voor SharePoint on-premises developers is het een enorme omslag hoe dingen nu ontwikkeld worden met Javascript. Je krijgt te maken met NodeJS, NPM, gulp, Yeoman, etc. om maar wat termen te noemen. Als je ervaring hebt met frontend-ontwikkelen, dan ligt de moeilijkheid in het begrijpen van de (misschien onlogische) concepten van SharePoint Online.</p>
<p>Door simpelweg de instructies te volgen die te vinden is op <a href="https://docs.microsoft.com" target="_blank" rel="noopener">https://docs.microsoft.com</a> kom je er niet helemaal. Wat ik recent bent tegengekomen is dat er fouten staan omdat de gebruikte tooling online vernieuwd zijn, maar nog niet verwerkt zijn in het SharePoint Framework. Dit geeft als resultaat dat de uitgevoerde commando’s succesvol gelukt zijn met fouten….</p>
<p><img src="/images/getting-started-with-spfx/why-successfull-with-errors.png"></p>
<p>De oplossing: Wees niet eigenwijs door altijd de laatste versie te downloaden omdat het beter is (I did this…), maar download specifieke oude versies want onderhoud van gerelateerde componenten is complex. Je doel is om een webpart te maken wat uiteindelijk javascript is. Ik vertrouw erop dat Microsoft dit op zijn tijd zal vernieuwen.</p>
<p>Om heel wat frustraties (die ik had) je te besparen, heb ik de volgende stappen opgesteld zodat je als startende SPFx-developer snel van start kan gaan met het SharePoint Framework:</p>
<ul>
<li>Zet eerst een gratis Developer Tenant op volgens deze instructies:<br><a href="https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant" target="_blank" rel="noopener">https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant</a>.</li>
<li>Maak je development omgeving op volgens deze instructies, maar sla <strong>Trusting the self-signed developer certificate</strong> over:<br><a href="https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-development-environment" target="_blank" rel="noopener">https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-development-environment</a>.</li>
<li>Noteer de benodigde urls die je vaak nodig zult hebben<br><strong>Developer-account</strong>: <em>[my-name]@[my-tenant].onmicrosoft.com</em><br><strong>SharePoint Admin-Tenant</strong>: <em>https://[my-tenant]-admin.sharepoint.com</em><br><strong>App Catalog</strong>: <em>https://[my-tenant].sharepoint.com/sites/appcatalog/SitePages/Introductiepagina.aspx</em><br><strong>(Modern) Developer Site</strong>: <em>https://[my-tenant].sharepoint.com/sites/DeveloperModern</em></li>
<li>Installeer NodeJS 10.15.3 (geen nieuwere versie):<br><a href="https://nodejs.org/dist/v10.15.3/node-v10.15.3-x6msi" target="_blank" rel="noopener">https://nodejs.org/dist/v10.15.3/node-v10.15.3-x6msi</a></li>
<li>Installeer Python 2.7 (geen nieuwere versie):<br><a href="https://www.python.org/download/releases/2.7/" target="_blank" rel="noopener">https://www.python.org/download/releases/2.7/</a></li>
<li>In de command-prompt, maak nieuwe folder aan waar je de sources wilt hebben.<br>Voor de niet-DOS generatie developers hier de commando’s om op je C-schijf in de root een map aan te maken:<br><strong>C:</strong><br><strong>md myfirstwebpart</strong><br><strong>cd myfirstwebpart</strong></li>
<li>SharePoint project aanmaken met volgende commando:<br><strong>yo @microsoft/sharepoint</strong><br>Beantwoord vervolgens de vragen die gesteld worden.</li>
<li>Voor de zekerheid alle packages installeren van dit project met volgende commando:<br><strong>Npm install</strong></li>
<li>Development certificaat vertrouwen met volgende commando:<br><strong>gulp trust-dev-cert</strong><br><img src="/images/getting-started-with-spfx/dev-spfx-cert.png"><br>Opmerking: Op de site van docs.microsoft.com stond deze stap boven het maken van een nieuw SPFx-project met yo@microsoft/sharepoint. Niet handig als je nog niet weet dat je een nieuw project gaat maken en zelf mag kiezen.<br><img src="/images/getting-started-with-spfx/trust-self-signed-dev-cert.png"></li>
<li>Bundel alle assets voor development doeleinde (gehost op <a href="https://localhost:xxxx" target="_blank" rel="noopener">https://localhost:xxxx</a>) met volgende commando:<br><strong>gulp bundle</strong></li>
<li>Maak de SharePoint-package voor development doeleinde (gehost op <a href="https://localhost:xxxx" target="_blank" rel="noopener">https://localhost:xxxx</a>) met volgende commando:<br><strong>gulp package-solution</strong><br>De uitvoer van dit commando komt terecht in de map ./sharepoint/solution<br>De SharePoint-package is het bestand eindigend met <strong>.sppkg</strong>.</li>
<li>Start je lokale webserver en de Workbench met het volgende commando:<br><strong>Gulp serve</strong></li>
</ul>
<p>Nu kun je aan de slag met Workbench welke automatisch gestart wordt. Je kunt het webpart toevoegen op de pagina en beginnen met ontwikkelen in Visual Studio Code. Let op: Je hebt nu geen beschikking tot data of SharePoint API’s. Het is de bedoeling dat je eerst de UI ontwikkelt en fictieve data maakt en gebruikt. Zorg dat <strong>gulp serve</strong> in de command-prompt blijft draaien. Dit is je lokale webserver van je webpart! Elke keer als je iets aanpast, compileert <strong>gulp serve</strong> je code en ververst automatisch in je browser de Workbench.</p>
<p><strong>Klaar met mockdata en wil je ontwikkelen in SharePoint Online zodat je over de echte data beschikt van Graph en SharePoint?</strong></p>
<ul>
<li>Voer de volgende commando’s uit:<br><strong>gulp bundle</strong><br><strong>gulp package-solution</strong></li>
<li>Upload en ‘implementeer’ (installeer) de SharePoint-package (.sppkg) naar je eerder ingerichte App-Catalog site:<br>bijv.: https://[my-tenant].sharepoint.com/sites/appcatalog/AppCatalog/Forms/AllItems.aspx</li>
</ul>
<p>Nu kun je de app installeren op elke SharePoint site als site beheerder en vervolgens de webpart op een willekeurige plek in je site plaatsen. Je hebt dan ook volledig beschikking tot de API’s van Graph en SharePoint.<br><em>Let op dat je webpart ‘werkt’ zolang <strong>gulp serve</strong> op de achtergrond draait en als iemand op een ander apparaat dezelfde pagina benaderdt, deze een technische foutmelding krijgt</em></p>
<p><strong>Klaar met ontwikkelen en wil je je ontwikkelde webpart deployen naar Test, Acceptatie en Productie?</strong></p>
<ul>
<li>Voer de volgende commando’s uit:<br><strong>gulp bundle –ship</strong><br><strong>gulp package-solution –ship</strong></li>
<li>Upload en ‘implementeer’ (installeer) de SharePoint-package (.sppkg) naar de App-Catalog site van gewenste tenant:<br>bijv.: https://[test/acc/prod-tenant].sharepoint.com/sites/appcatalog/AppCatalog/Forms/AllItems.aspx</li>
</ul>
<p>Nu kun je de app installeren op elke SharePoint site als site beheerder en vervolgens de webpart op een willekeurige plek in je site plaatsen. Iedereen kan de webpart gebruiken en ook op telefoon en tablet via de SharePoint App.</p>
</div>
<footer class="article-footer">
<a data-url="https://cloudrepublic.github.io/2019/06/14/Getting-started-with-SPfx/" data-id="cjx635wve0000bcuny9o10sds" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Frontend/" rel="tag">Frontend</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Gulp/" rel="tag">Gulp</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Office-365/" rel="tag">Office 365</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/React/" rel="tag">React</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/SPFx/" rel="tag">SPFx</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/SharePoint-Framework/" rel="tag">SharePoint Framework</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/SharePoint-Online/" rel="tag">SharePoint Online</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Webpart/" rel="tag">Webpart</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Workbench/" rel="tag">Workbench</a></li></ul>
</footer>
</div>
</article>
<article id="post-follow-the-moon" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2019/05/22/follow-the-moon/" class="article-date">
<time datetime="2019-05-22T16:00:00.000Z" itemprop="datePublished">2019-05-22</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2019/05/22/follow-the-moon/">Following the moon</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>In the ideal situation, we would like to release our new code with zero downtime. It is very possible that this can be done by releasing to a separate staging slot and then swapping this with the production slot. Should be zero downtime. However, there are always cases where this does not apply, where downtime can only be avoided through tedious manual intervention and multiple failover steps. Say we have an application that runs globally and we have such a case. Or if we just really, really want to make sure that our customers have as few problems as possible.</p>
<p>In this case, we propose to use the follow-the-moon (FTM) release schedule. This means releasing our application at times in different regions where for each region, the time we release at is the time where it is least likely that a customer is using the application. And yes, we know that the moon can be visible during the day, but you get the sentiment.</p>
<p>You will still want to have the entry point of your application to send your users to a region you host your application in that is 1) available and 2) close to the user. Taking a region down to release a new version of your application is still not desired, as it will either not be rerouted (e.g. because of caching) and thus will result in routing to an application that is down, or be rerouted to a region where the distance can cause undesirable increases in response times. Thus, we would like to make sure to do the release at a time where it is convenient per region, not all at once.</p>
<p>In the FTM release schedule, we may, for example, set the release time for each region to 03:00 local time. Once we approve the continuation of the release, the schedule will start to kick in and release our application to all regions at their respective optimal times. This means that our application is rolled out automatically, in different time zones, without the need for manual intervention in our multi-region roll-out process.</p>
<p>Getting our application to production globally could involve the following:</p>
<ul>
<li>Set up a continuous integration build to automate building your code</li>
<li>Set up an automatic release pipeline, automatically started from artifacts, going through multiple stages with certain filters on source branches</li>
<li>Set up the follow-the-moon release schedule for our production releases to multiple regions</li>
</ul>
<h2 id="Getting-to-the-moon"><a href="#Getting-to-the-moon" class="headerlink" title="Getting to the moon"></a>Getting to the moon</h2><p>For this part, we assume your project has some code and a build is in place to create artifacts which we can work with.</p>
<p>Suppose our release pipeline looks like this:</p>
<p><img src="/images/follow-the-moon/follow-the-moon-1.jpg"></p>
<p>We have our artifact as an entry point. We have our DEV and TST environments hooked up for continuous releases based on the develop branch. Finally, we have our ACC and PRD environments hooked up for continuous releases based on the master branch. In this case, we want to double-check the ACC environment before actually rolling out to PRD, so we add a post-deployment approval condition there.</p>
<p>Now, if we click on the pre-deployment conditions, we see the following menu:</p>
<p><img src="/images/follow-the-moon/follow-the-moon-2.jpg"></p>
<p>We enable the schedule and set it to the time where we expect our users to not use the application in that region. For example, in the WE (West Europe) Azure region, at 03:00 would be when we expect our customers to sleep, so we may decide that this is the right time to deploy.</p>
<p><img src="/images/follow-the-moon/follow-the-moon-3.jpg"></p>
<p>After we have done this for all the production environments, we have successfully implemented the FTM release schedule! Do note that using swap slots is still what you want to do. However, this principle gives us a little bit of extra safety when releasing code that may otherwise cause downtime.</p>
</div>
<footer class="article-footer">
<a data-url="https://cloudrepublic.github.io/2019/05/22/follow-the-moon/" data-id="cjx635wvi0001bcunou5oi6k1" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Azure-DevOps/" rel="tag">Azure DevOps</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Release-pipeline/" rel="tag">Release pipeline</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Release-schedule/" rel="tag">Release schedule</a></li></ul>
</footer>
</div>
</article>
<article id="post-Microsoft-Build-2019-Must-Watch-Guide" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2019/05/09/Microsoft-Build-2019-Must-Watch-Guide/" class="article-date">
<time datetime="2019-05-09T16:00:00.000Z" itemprop="datePublished">2019-05-09</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2019/05/09/Microsoft-Build-2019-Must-Watch-Guide/">Microsoft Build 2019 Must Watch Guide</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p></p><p>6 Mei tot 8 Mei vond het jaarlijkse event voor Microsoft ontwikkelaars plaats, namelijk: Microsoft Build.<br>Cloud Republic heeft dit evenement op de voet gevolgd en heeft daarbij een selectie gemaakt van de highlights die wij de moeite waard vinden.</p><p></p>
<p></p><p>Gaat u zitten voor grofweg een werkdag aan video’s. Even een avondje geen Netflix maar Channel 9!</p><p></p>
<div style="overflow: auto;"><div style="float: left; width: 50%"><br> <a href="https://mybuild.techcommunity.microsoft.com/sessions/77571?source=%7B%7B%20ctrl.source" target="_blank" rel="noopener"><strong>Vision Keynote - Satya Nadella</strong></a><br> <img src="/images/msbuild2019/keynote.jpg" style="width: 100%;"><br> De keynote van Satya moet je natuurlijk gezien hebben. Hij was niet zo technisch als andere jaren maar zeker de moeite waard om te bekijken. In grote lijnen legt Satya uit welke koers Microsoft vaart. Altijd goed om dat scherp te hebben.<br></div><div style="float: left; width: 50%;"><br> <a href="https://mybuild.techcommunity.microsoft.com/sessions/77572?source=%7B%7B%20ctrl.source" target="_blank" rel="noopener"><strong>Microsoft Azure: Empowering Every Developer - Scott Guthrie</strong></a><br> <img class="nofancybox" src="/images/msbuild2019/azure_keynote.jpg" style="width: 100%;"><br> Scott Guthrie is de grote baas van Azure. De populariteit van Azure staat als een paal boven water. In deze sessie gaat Scott door een scala van nieuwe features voor Azure en AzureDevops.<br></div></div><br><div style="overflow: auto;"><div style="float: left; width: 50%;"><br> <a href="https://mybuild.techcommunity.microsoft.com/sessions/77031?source=sessions" target="_blank" rel="noopener"><strong>.NET Platform Overview and Roadmap - Scott Hunter and Scott Hanselman</strong></a><br> <img src="/images/msbuild2019/net_overview.jpg" style="width: 100%;"><br> .NET is en blijft toch wel het development platform van Microsoft. In deze sessie nemen de “Lesser Scotts” je mee in de nieuwe features van .NET (Core) en geven ze een inkijkje in de toekomst van .NET. (Deze sessie bevat informatie over de aankondiging van .NET 5.0)<br></div><div style="float: left; width: 50%;"><br> <a href="https://mybuild.techcommunity.microsoft.com/sessions/77123?source=sessions" target="_blank" rel="noopener"><strong>All the Developer Things with Hanselman and Friends - Scott Hanselman</strong></a><br> <img src="/images/msbuild2019/all_the_developer_things.jpg" style="width: 100%;"><br> Scott Hanselman is naast een uitstekende developer ook een halve komiek. Zijn sessies zijn vaak informatief en hilarisch tegelijk, zo ook deze. Veel kijk plezier!<br></div></div><br><div style="overflow: auto;"><div style="float: left; width: 50%"><br> <a href="https://mybuild.techcommunity.microsoft.com/sessions/77385?source=sessions" target="_blank" rel="noopener"><strong>Look back up C# - Andres Hejlsberg</strong></a><br> <img src="/images/msbuild2019/look_back.jpg" style="width: 100%;"><br> Andres Hejlsberg is één van de “Technical Fellows” van Microsoft. Met andere woorden hij is de God Father van C# en TypeScript. Zijn visie op programmeertalen is ongekend, als je iets wilt leren over language design, compilers en dergelijke. Kijk dan zijn sessies.<br></div><div style="float: left; width: 50%;"><br> <a href="https://mybuild.techcommunity.microsoft.com/sessions/77040?source=sessions" target="_blank" rel="noopener"><strong>Whats new in TypeScript - Daniel Rosenwasser</strong></a><br> <img src="/images/msbuild2019/whats_new_in_typescript.jpg" style="width: 100%;"><br> Wie schrijft er nog plain JavaScript? Velen zijn al over naar TypeScript, wij ook. Daniel Rosenwasser neemt je mee in de wonderen wereld van TypeScript. Ik durf te wedden dat je 50% van de TypeScript features nog niet gebruikt.<br></div></div><br><div style="overflow: auto;"><div style="float: left; width: 50%"><br> <a href="https://mybuild.techcommunity.microsoft.com/sessions/77336?source=sessions" target="_blank" rel="noopener"><strong>Serverless web apps with Blazor, Azure Functions… - Jeff Hollan</strong></a><br> <img src="/images/msbuild2019/serverless_webapps.jpg" style="width: 100%;"><br> Wil je een sessie met veel nieuwe techniek? Kijk dan deze sessie van Jeff Holan. Jeff is de Program Manager van Azure Functions en bouwt in deze sessie een Blazor (C# Web Assembly) applicatie bovenop Azure Functions.<br></div><div style="float: left; width: 50%;"><br> <a href="https://mybuild.techcommunity.microsoft.com/sessions/77002?source=sessions" target="_blank" rel="noopener"><strong>Inside Azure datacenter architecture - Mark Russinovich</strong></a><br> <img src="/images/msbuild2019/inside_azure_data_centre.jpg" style="width: 100%;"><br> Deze sessie moet je gewoon kijken als je ontwikkelt in Azure. Mark Russinovich is de CTO van Azure en hij geeft je in deze sessie een inkijkje in de data centers van Azure.<br></div></div>
</div>
<footer class="article-footer">
<a data-url="https://cloudrepublic.github.io/2019/05/09/Microsoft-Build-2019-Must-Watch-Guide/" data-id="cjx635wvq0003bcunqnxue8yl" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Microsoft-Build/" rel="tag">Microsoft Build</a></li></ul>
</footer>
</div>
</article>
<article id="post-Proof-of-concept-met-durable-functions" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2019/03/28/Proof-of-concept-met-durable-functions/" class="article-date">
<time datetime="2019-03-28T18:54:37.000Z" itemprop="datePublished">2019-03-28</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2019/03/28/Proof-of-concept-met-durable-functions/">Proof of concept met durable functions</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<h2 id="Case-omschrijving"><a href="#Case-omschrijving" class="headerlink" title="Case omschrijving "></a>Case omschrijving </h2><p>Voor een klant heb ik een Proof of Concept gemaakt om grote hoeveelheden XML bestanden te transformeren het gaat dan 3000 bestanden per keer met een totale grote van 18 gigabyte. Voor dit artikel kan ik wegens privacy niet de echte data gebruiken en heb ik een dataset gebruikt van <a href="https://www.kaggle.com/datasets" target="_blank" rel="noopener">https://www.kaggle.com/datasets</a> het gaat hier om een dataset met landen en de wijnen welke uit het desbetreffende land komen. </p>
<p>Het doel is om de wijnen uit de XML dump te halen en deze om te zetten naar een JSON formaat en deze bestanden te uploaden in een blob container. We krijgen dus per wijn een JSON bestand in een blob container. Dit alles moet gebeuren op basis van durable functions en een constumption plan.</p>
<h2 id="Wat-is-de-opzet-van-de-POC"><a href="#Wat-is-de-opzet-van-de-POC" class="headerlink" title="Wat is de opzet van de POC"></a>Wat is de opzet van de POC</h2><p>Ik ga hier niet heel diep in op wat Durable functions zijn want dat heeft Microsoft heel goed omschreven in hun documentatie <a href="https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview" target="_blank" rel="noopener">https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview</a>.</p>
<p>Ik heb 40 bestanden met landen en wijnen. 1 bestand is ongeveer 75 mb groot. Voor test doeleinde zijn dit dezelfde bestanden met een andere naam. Dit is meer om een gelijkwaardige load op de functie te krijgen als bij de echte POC.</p>
<p>Ik maak in de POC gebruik ik een <em>extract countries</em> activity om de landen uit de XML te halen. Hierna ga met het <em>fan out</em> principe ik per land een activity starten om de wijnen per land uit structuur te halen. Als dit allemaal klaar is wordt er per wijn een activity gestart welke de wijn in JSON formaat upload in een blob container.</p>
<p>Zie hier een overzicht van de durable function:<br><img src="/images/durable-functions-overview.png"></p>
<h2 id="Waar-ben-ik-tegenaan-gelopen-tijdens-de-POC"><a href="#Waar-ben-ik-tegenaan-gelopen-tijdens-de-POC" class="headerlink" title="Waar ben ik tegenaan gelopen tijdens de POC"></a>Waar ben ik tegenaan gelopen tijdens de POC</h2><p>Op papier leek dit de meest perfecte oplossing ik kon de landen in een activity uit de dump halen en dan per land een activity starten om de wijnen op te halen.<br>Dit geeft je een goede schaalbaarheid, als er meer landen in komen worden er meer <em>extract wine</em> activitiy taken aangemaakt.<br>Als er meer wijnen per land komen worden er meerdere <em>upload</em> activity taken aangemaakt.</p>
<p>Per bestand gaat de blob trigger af op de functie. De blob triggers welke niet meteen afgehandeld kunnen worden worden in een queue opgeslagen in het storage account van de function. Deze queue heeft een naam welke begint met <em>azure-webjobs-blobtrigger-</em></p>
<p>Als het bestand binnenkomt wordt er een orchestrator opgestart welke de blobnaam doorgeeft aan de orchestrator.<br><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">[<span class="meta">FunctionName(<span class="meta-string">"WineFunction"</span>)</span>]</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">Run</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> [BlobTrigger(<span class="string">"wine/{name}"</span></span>)]Stream myBlob,</span></span><br><span class="line"><span class="function"> <span class="keyword">string</span> name,</span></span><br><span class="line"><span class="function"> [OrchestrationClient]DurableOrchestrationClient starter,</span></span><br><span class="line"><span class="function"> ILogger log)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> log.LogDebug(<span class="string">$"Process file: <span class="subst">{name}</span>"</span>);</span><br><span class="line"> <span class="keyword">await</span> starter.StartNewAsync(<span class="string">"O_Orchestrator"</span>, name);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>De orchestrator start de activity <em>A_ExtractWineCountries</em> op om de XML op land niveau op te knippen en wacht tot dit klaar is.<br><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">[<span class="meta">FunctionName(<span class="meta-string">"O_Orchestrator"</span>)</span>]</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">Orchestrator</span>(<span class="params">[OrchestrationTrigger] DurableOrchestrationContext context, ILogger log</span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">var</span> fileName = context.GetInput<<span class="keyword">string</span>>();</span><br><span class="line"> <span class="keyword">var</span> wineData = <span class="keyword">await</span> context.CallActivityAsync<Countries[]>(<span class="string">"A_ExtractWineCountries"</span>, fileName);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>Als dit klaar is wordt er per land de activity <em>A_ExtractWines</em> gestart om de wijnen uit de data te halen.<br><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">[<span class="meta">FunctionName(<span class="meta-string">"O_Orchestrator"</span>)</span>]</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">Orchestrator</span>(<span class="params">[OrchestrationTrigger] DurableOrchestrationContext context, ILogger log</span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">var</span> fileName = context.GetInput<<span class="keyword">string</span>>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> wineCountries = <span class="keyword">await</span> context.CallActivityAsync<Countries[]>(<span class="string">"A_ExtractWineCountries"</span>, fileName);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> tasks = <span class="keyword">new</span> List<Task<Wine[]>>();</span><br><span class="line"> <span class="keyword">foreach</span> (Countries wineCountry <span class="keyword">in</span> wineCountries)</span><br><span class="line"> {</span><br><span class="line"> tasks.Add(context.CallActivityAsync<Wine[]>(<span class="string">"A_ExtractWines"</span>, wineCountry));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> wineTasks = <span class="keyword">await</span> Task.WhenAll(tasks);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>Als alle wijn data is opgehaald worden deze parallel geupload in de <em>A_UploadWine</em> activity. </p>
<figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">[<span class="meta">FunctionName(<span class="meta-string">"O_Orchestrator"</span>)</span>]</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">Orchestrator</span>(<span class="params">[OrchestrationTrigger] DurableOrchestrationContext context, ILogger log</span>)</span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">var</span> fileName = context.GetInput<<span class="keyword">string</span>>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> wineCountries = <span class="keyword">await</span> context.CallActivityAsync<Countries[]>(<span class="string">"A_ExtractWineCountries"</span>, fileName);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> tasks = <span class="keyword">new</span> List<Task<Wine[]>>();</span><br><span class="line"> <span class="keyword">foreach</span> (Countries wineCountry <span class="keyword">in</span> wineCountries)</span><br><span class="line"> {</span><br><span class="line"> tasks.Add(context.CallActivityAsync<Wine[]>(<span class="string">"A_ExtractWines"</span>, wineCountry));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> wineTasks = <span class="keyword">await</span> Task.WhenAll(tasks);</span><br><span class="line"></span><br><span class="line"> List<Task> uploadWineTasks = <span class="keyword">new</span> List<Task>();</span><br><span class="line"> <span class="keyword">foreach</span> (Wine wineTask <span class="keyword">in</span> wineTasks.SelectMany(x => x))</span><br><span class="line"> {</span><br><span class="line"> uploadWineTasks.Add(context.CallActivityAsync(<span class="string">"A_UploadWine"</span>, wineTask));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> Task.WhenAll(uploadWineTasks);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!context.IsReplaying)</span><br><span class="line"> {</span><br><span class="line"> log.LogDebug(<span class="string">$"Finished file: <span class="subst">{fileName}</span>"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>Tot zover ziet het er goed en schaalbaar uit tot ik het publiceerde naar een Azure function app.<br>Ik kopieer met azcopy 40 bestanden van 75 mb in de blob container <em>wine</em> en start application insights op om te zien hoe de applicatie zich gedraagt.<br><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">AzCopy /Source:https://sourceaccount.blob.core.windows.net/source /Dest:https://destaccount.blob.core.windows.net/wine /SourceKey:key1 /DestKey:key2 /S</span><br></pre></td></tr></table></figure></p>
<p>Het resultaat wat ik te zien kreeg maakte me alles behalve blij. De applicatie liep niet soepel en stopte soms geheel met het verwerken van bestanden.<br>Na lang zoeken en fine tunen kwam ik erachter wat een grote oorzaak van het probleem was.</p>
<p>Een Azure function app op een consumption plan heeft een geheugen limiet van 1,5 gigabyte en 1 processor core. Standaard draait Azure durable functions 10X het aantal activities als de host proecessor cores heeft (bij een consumption plan is dit dus 10 activities) en als er toevallig meerdere activities <em>A_ExtractWineCountries</em> worden gestart worden er meerdere XML bestanden in het geheugen geladen a 75 mb. Reken hierbij nog de orchestrator bij en het overige geheugen verbruik van een function app en je zit al snel aan 1,5 gig geheugen.</p>
<p><img src="/images/durable-functions-applicationInsight.png"></p>
<p>Zie het geheugen verbruik van 1 server zit al op 1112MB deze server.</p>
<p>Als je de geheugen limiet hebt bereikt wordt de server gestopt en worden de taken welke op de function app draaide niet afgemaakt deze worden deze opnieuw gestart op een andere function app. </p>
<p>Je kan instellen hoeveel activities en orchestrators er op een function app gestart mogen worden. Dit kun je doen in de host.config (<a href="https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-bindings#hostjson-settings)" target="_blank" rel="noopener">https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-bindings#hostjson-settings)</a>. Je kan de waardes aanpassen naar een lagere waarde alleen loopt het proces niet lekker stabiel door. Doordat het niet lekker stabiel doorloopt en dit als gevolg heeft dat de cpu belasting op en neer gaat gaat het automatisch schalen niet soepel.<br><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"extensions"</span>: {</span><br><span class="line"> <span class="attr">"durableTask"</span>: {</span><br><span class="line"> <span class="attr">"maxConcurrentActivityFunctions"</span>: <span class="number">10</span>,</span><br><span class="line"> <span class="attr">"maxConcurrentOrchestratorFunctions"</span>: <span class="number">10</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<h2 id="Demoproject"><a href="#Demoproject" class="headerlink" title="Demoproject"></a>Demoproject</h2><p>Ik heb een demo project gemaakt welke een volledig werkende solution bevat.<br>Zie hieronder de stappen om het durable functions project te starten:</p>
<ul>
<li>Clone de repository <a href="https://github.com/marcoippel/durablefunctionsdemo" target="_blank" rel="noopener">https://github.com/marcoippel/durablefunctionsdemo</a></li>
<li>Maak een storage account aan in Azure</li>
<li>Maak een functions app aan in Azure en configureer de connectionstring in de appsettings </li>
<li>Maak in de blob storage een 3 tal blob containers aan genaamd:<ul>
<li>source</li>
<li>wines</li>
<li>wine</li>