<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>.:: Marcos Dione/StyXman's glob ::. (Posts about flask)</title><link>https://www.grulic.org.ar/~mdione/glob/</link><description></description><atom:link href="https://www.grulic.org.ar/~mdione/glob/categories/flask.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2025 &lt;a href="mailto:mdione@grulic.org.ar"&gt;Marcos Dione&lt;/a&gt; </copyright><lastBuildDate>Sat, 15 Nov 2025 20:52:05 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Writing a tile server in python</title><link>https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/</link><dc:creator>Marcos Dione</dc:creator><description>&lt;p&gt;Another dictated post&lt;sup id="fnref:0"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:0"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref:13"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:13"&gt;11&lt;/a&gt;&lt;/sup&gt;, but heavily edited. Buyer beware.&lt;/p&gt;
&lt;p&gt;I developed a tileset based on OpenStreetMap data and style and elevation information, but I don't have a render server.
What I have been doing is using &lt;a href="https://github.com/StyXman/osm-tile-tools"&gt;my own version&lt;/a&gt; of an old script from the
&lt;code&gt;mapnik&lt;/code&gt; version of the OSM style. This
script is called &lt;code&gt;generate_tiles&lt;/code&gt;, and I made big modifications to it and now it's capable of doing many things,
including spawning several processes for handling the rendering. You can define regions that you want to render, or you
can just provide a bbox or a set of tiles or just coordinates. You can change the size of the meta tile, and it handles
empty tiles. If you find a sea tile, most probably you will not need to render its children&lt;sup id="fnref:10"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:10"&gt;9&lt;/a&gt;&lt;/sup&gt;, where children are the
four tiles that are just under it in the next zoom level. For instance, in zoom level zero we have only one tile (0,0,0),
and it's children are (1,0,0), (1,0,1), (1,1,0) and (1,1,1). 75% of the planet's surface is water, and with Mercator
projection and the &lt;a href="https://en.wikipedia.org/w/index.php?title=Southern_Ocean"&gt;Antartic Ocean&lt;/a&gt;, the percent of tiles
could be bigger, so this optimization cuts a lot of useless rendering time.&lt;/p&gt;
&lt;p&gt;Another optimization is that it assumes that when you render zoom level N, you will be using
at least the same data for zoom level N+1. Of course, I am
not catching that data because &lt;code&gt;mapnik&lt;/code&gt; does not allow this, but the operating system does the catching. So if
you have enough RAM, then you should be able to reuse all the data that's already in buffers and cache, instead of
having to fetch them again from disk. This in theory should accelerate rendering and probably it is&lt;sup id="fnref:11"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:11"&gt;10&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The script works very well, and I've been using it for years already for rendering tiles in batches for several zoom
levels. Because my personal computer is way more powerful than
my server (and younger; 2018 vs 2011), I render in my computer and &lt;code&gt;rsync&lt;/code&gt; to my server.&lt;/p&gt;
&lt;p&gt;So now I wanted to make a tile server based on
this. Why do I want to make my own and not use &lt;code&gt;renderd&lt;/code&gt;? I think my main issue with &lt;code&gt;renderd&lt;/code&gt; is that it does not store
the individual tiles, but keeps metatiles of 8x8 tiles and serve the individual tiles from there. This saves inode usage
and internal fragmentation. Since my main usage so far has been (and probably will continue to be) rendering regions by
hand, and since my current (static) tile server stores all the latest versions of the tiles I have rendered since I
started doing this some 15 years ago, I want updating the server in a fast way. Most tile storage methods I know
fail terribly at update time (&lt;a href="https://en.osm.town/@mdione/111069567039016636"&gt;see here&lt;/a&gt;); most of the time it means
sending the whole file over the wire. Also, individual
tiles are easier to convert to anything else, like creating a MBTiles file, push it to my phone, and have a offline
tile service I can carry with me on treks where there is no signal. Also, serving the tiles can be as easy as
&lt;code&gt;python -m http.server&lt;/code&gt; from the tileset root directory. So &lt;code&gt;renderd&lt;/code&gt; is not useful for me. Another reason is, well, I
already have the rendering engine working. So how does it work?&lt;/p&gt;
&lt;p&gt;The rendering engine consists
of one main thread, which I call Master, and rendering threads&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:2"&gt;3&lt;/a&gt;&lt;/sup&gt;. These rendering threads load the style and wait for
work to do. The current style file is 6MiB+ and takes &lt;code&gt;mapnik&lt;/code&gt; 4s+ to load it and generate all its structures, which
means these threads have to be created once per service lifetime. I have one queue that can send commands from the
Master to the renderer pool asking for rendering a metatile, which is faster than rendering the individual tiles. Then
one of the rendering threads picks the request from this queue, calls &lt;code&gt;mapnik&lt;/code&gt;, generates the metatile, cuts it into
the subtiles and saves them to disk. The rendering thread posts in another queue, telling the Master about the children
metatiles that must be rendered, which due to emptiness can be between 0 and 4.&lt;/p&gt;
&lt;p&gt;To implement the caching optimization I mentioned before, I use a third structure to maintain a stack. At the beginning
I push into it the initial work; later I pop one element from it, and when a rendered returns the list of children to be
rendered, I push them on top of the rest. This is what tries to guarantee that a metatile's children will be rendered
before moving to another region that would trash the cache. And because the children can inspect the tiles being written,
they can figure out when a child is all sea tiles and not returning it for rendering.&lt;/p&gt;
&lt;p&gt;At the beginning I thought that, because the multiprocessing queues are implemented with pipes, I could use &lt;code&gt;select()&lt;/code&gt;&lt;sup id="fnref:3"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:3"&gt;4&lt;/a&gt;&lt;/sup&gt; to
see whether the queue was ready for writing or reading and use a typical non-blocking loop. When you're trying
to write, these queues will block when the queue is full, and when you're trying to read, they will block when the queue
is empty. But these two conditions, full and empty, are actually handled by semaphores, not by the size of the pipe.
That means that selecting on those pipes, even if I could reach all the way down into the structures of the
&lt;code&gt;multiprocessing.Queue&lt;/code&gt; all the way down. and add them to a selector, yes, the read queue will not be selected if it's
empty (nothing to read), but the write queue will not, since availability of space in the pipe does not mean the queue
is not full.&lt;/p&gt;
&lt;p&gt;So instead I'm peeking into these queues. For the work queue, I know that the Master thread&lt;sup id="fnref:9"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:9"&gt;8&lt;/a&gt;&lt;/sup&gt; is the only writer, so I can peek to
see if it is full. If it is, I am not going to send any new work to be done, because it means that all the renders are
busy, and the only work queued to be done has not been picked up yet. For the reading side it's the same, Master is the
only reader. so, I can peek if it's empty, and if it is, I am not going to try to read any information from it. So, I
have a loop, peeking first into the work queue and then into the info queue. If nothing has been done, I sleep a
fraction of a second.&lt;/p&gt;
&lt;p&gt;Now let's try to think about how to replace this main loop with a web frontend. What is the web frontend going to do?
It's going to be getting queries by different clients. It could be just a slippy map in a web page, so we have a browser
as a client, or it could be any of the applications that can also render slippy maps. For instance, on Linux, we have
&lt;code&gt;marble&lt;/code&gt;; on Android, I use MyTrails, and OsmAnd.&lt;/p&gt;
&lt;p&gt;One of the things about these clients is that they have timeouts. Why
am I mentioning this? Because rendering a metatile for me can take between 3 to 120 seconds, depending on the zoom level.
There are zoom levels that are really, really expensive, like between 7 and 10. If a client is going to be asking
directly a rendering service for a tile, and the tile takes too long to render, the
client will timeout and close the connection. How do we handle this on the server side? Well, instead of the work stack,
the server will have request queue, which will be collecting the requests from the clients, and the Master will be
sending these  requests to the render pool.&lt;/p&gt;
&lt;p&gt;So if the client closes the connection, I want to be able to react to that, removing any lingering requests made by that
client from the request queue. If I don't do that, the request queue will start piling up more and more requests,
creating a denial of service. This is not possible in multiprocessing queues, you cannot remove an element. The only
container that can do that is a dequeue&lt;sup id="fnref:5"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:5"&gt;5&lt;/a&gt;&lt;/sup&gt;, which also is optimized for putting and popping things from both ends (it's
probably implemented using a circular buffer), which is perfect for a queue. As for the info queue, I will not be caring
anymore about children metatiles, because I will not be doing any work that the clients are not requesting.&lt;/p&gt;
&lt;p&gt;What framework that would allow me to do this? Let's recap the requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Results are computed, and take several seconds.&lt;/li&gt;
&lt;li&gt;The library that generates the results is not async, nor thread safe, so I need to use subprocesses to achieve
  parallelization.&lt;/li&gt;
&lt;li&gt;A current batch implementation uses 2 queues to send and retrieve computations to a pool of subprocesses; my idea is
  to "just" add a web frontend to this.&lt;/li&gt;
&lt;li&gt;Each subprocess spends some seconds warming up, son I can't spawn a new process for each request.&lt;/li&gt;
&lt;li&gt;Since I will have a queue of requested computations, if a client dies, if its query is being processed, then I let
  it finish; if not, I should remove it from the waiting queue.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I started with FastAPI, but it doesn't have the support that I need. At first I just implemented a tile server; the idea
was to grow from there&lt;sup id="fnref:6"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:6"&gt;6&lt;/a&gt;&lt;/sup&gt;, but reading the docs it only allows doing long running async stuff &lt;em&gt;after&lt;/em&gt; the response has
been sent.&lt;/p&gt;
&lt;p&gt;Next was Flask. Flask is not async unless you want to use &lt;code&gt;sendfile()&lt;/code&gt;. &lt;code&gt;sendfile()&lt;/code&gt; is a way to make the
kernel read a file and write it directly on a socket without intervention from the process requesting that. The
alternative is to to open the file, read a block, write it on the socket, repeat. This definitely makes your code more
complex, you have to handle lots of cases. So &lt;code&gt;sendfile()&lt;/code&gt; is very, very handy, but it's also faster because it's
0-copy. But Flask does not give control of what happens when the client suddenly closes the connection. I can
instruct it to cancel the tasks in flight, but as per all the previous explanation, that's not what I want.&lt;/p&gt;
&lt;p&gt;This same problem seems to affect all async frameworks I looked into. &lt;code&gt;asyncio&lt;/code&gt;, &lt;code&gt;aiohttp&lt;/code&gt;, &lt;code&gt;tornado&lt;/code&gt;. Except, of course,
&lt;code&gt;twisted&lt;/code&gt;, but its API for that is with callbacks, and TBH, I was starting to get tired of all this, and the prospect
of callback hell, even when all the rest of the system could be developed in a more async way, was too much. And this is
not counting the fact that I need to hook into the main loop to step the Master. This could be implemented with timed
callbacks, such as &lt;code&gt;twisted&lt;/code&gt;'s &lt;code&gt;callLater()&lt;/code&gt;, but another thought started to form in my head.&lt;/p&gt;
&lt;p&gt;Why did I go directly for frameworks? Because they're supposed to make our lives easier, but from the beginning I had
the impression that this would not be a run of the mill service. The main issue came down to beign able to send things
to render, return the rendered data to the right clients, associate several clients to a single job before it finished
(more than one client might request the same tile or several tiles that belong to the same metatile), and handle client
and job cancellation when clients disappear. The more frameworks' documentation I read, the more I started to fear that
the only solution was to implement an non-blocking&lt;sup id="fnref:14"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:14"&gt;12&lt;/a&gt;&lt;/sup&gt; loop myself.&lt;/p&gt;
&lt;p&gt;I gotta be honest, I dusted
&lt;a href="https://en.wikipedia.org/wiki/UNIX_Network_Programming?useskin=vector"&gt;an old Unix Network Programming book&lt;/a&gt;, 2nd Ed.,
1998 (!!!), read half a chapter, and I was ready to do it. And thanks to the simple &lt;code&gt;selector&lt;/code&gt; API, it's a breeze:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a listening socket.&lt;/li&gt;
&lt;li&gt;Register it for read events (connections).&lt;/li&gt;
&lt;li&gt;On connection, accept the client and wait for read events in that one too.&lt;/li&gt;
&lt;li&gt;We were not registering for write before because the client is always ready for write before we start sending
     anything, which lead to tight loops.&lt;/li&gt;
&lt;li&gt;On client read, read the request and send the job to Master. Unregister for read.&lt;/li&gt;
&lt;li&gt;But if there's nothing to read, the client disconnected. Send an empty.response, unregister for read and register
     for write.&lt;/li&gt;
&lt;li&gt;Step Master.&lt;/li&gt;
&lt;li&gt;If anything came back, generate the responses and queue them for sending. Register the right clients for write.&lt;/li&gt;
&lt;li&gt;On client write (almost always), send the response and the file with &lt;code&gt;sendfile()&lt;/code&gt; if any.&lt;/li&gt;
&lt;li&gt;Then close the connection and unregister.&lt;/li&gt;
&lt;li&gt;Loop to #3.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Initially all this, including reimplementing fake Master and render threads, took less than 200 lines of code, some 11h
of on-and-off work. Now that I have finished I have a better idea of how to implement this at least with &lt;code&gt;twisted&lt;/code&gt;,
which I think I will have to do, since step 4 assumes the whole query can be &lt;code&gt;recv()&lt;/code&gt;'ed in one go and step 7 similarly
for &lt;code&gt;send()&lt;/code&gt;'ing; luckily I don't need to do any handholding for &lt;code&gt;sendfile()&lt;/code&gt;, even when the socket is non blocking.
A more production ready service needs to handle short reads and writes. Also, the HTTP/1.1 protocol all clients are
using allows me to assume that once a query is received, the client will be waiting for an answer before trying anything
else, and that I can close the connection once a response has been send and assume the client will open a new connection
for more tiles. And even then, supporting keep alive should not be that hard (instead of closing the client, unregister
for write, register for read, and only do the close dance when the response is empty). And because I can simply step
Master in the main loop, I don't have to worry about blocking queues.&lt;/p&gt;
&lt;p&gt;Of course, now it's more complex, because it's implementing support for multiple clients with different
queries requiring rendering the same metatile. This is due that applications will open several clients for fetching
tiles when showing a region, and unless it's only 4 and they fall in the corner of 4 adjacent metatiles, they will always mean
more than one client per metatile. Also, I could have several clients looking at the same region.
&lt;a href="https://github.com/StyXman/osm-tile-tools/blob/master/rendering_tile_server-sockets.py"&gt;The current code&lt;/a&gt; is
approaching the 500 lines, but all that should also be present in any other implementation.&lt;/p&gt;
&lt;p&gt;I'm pretty happy about how fast I could make it work and how easy it was. Soon I'll be finishing integrating a real
render thread with saving the tiles and implement the fact that if one metatile's tile is not present, we can assume
it's OK, but if all are not present, I have to find out if they were all empty or never rendered. A last step would be
how to make all this testable. And of course, the &lt;code&gt;twisted&lt;/code&gt; port.
&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:0"&gt;
&lt;p&gt;This is getting out of hand. The audio was 1h long, not sure how long it took to auto transcribe, and when editing
  and thinking I was getting to the end of it, the preview told me I still had like half the text to go through. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:0" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;No idea what I wanted to write here :) &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:1" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Because &lt;code&gt;mapnik&lt;/code&gt; is not thread safe and because of the GIL, they're actually subprocesses via the
  &lt;code&gt;multioprocessing&lt;/code&gt; module, but I'll keep calling them threads to simplify. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:2" title="Jump back to footnote 3 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Again, a simplification. Python provides the selector module that allows using abstract implementations that
  spare us from having to select the best implementation for the platform. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:3" title="Jump back to footnote 4 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;I just found out it's pronounced like 'deck'. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:5" title="Jump back to footnote 5 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;All the implementations I did followed the same pattern. In fact, right now, I hadn't implementing the rendering
  tile server: it's only blockingly &lt;code&gt;sleep()&lt;/code&gt;'ing for some time (up to 75s, to trigger client timeouts), and then
  returning the tiles already present. What's currently missing is figuring out whether I should rerender or use the
  tiles already present&lt;sup id="fnref:7"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fn:7"&gt;7&lt;/a&gt;&lt;/sup&gt;, and actually connecting the rendering part. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:6" title="Jump back to footnote 6 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;Two reasons to rerender: the data is stale, or the style has changed. The latter requires reloading the styles,
  which will probably mean rebuilding the rendering threads. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:7" title="Jump back to footnote 7 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:9"&gt;
&lt;p&gt;I keep calling this the Master thread, but at this point instead of having its own main loop, I'm just calling a
  function that implements the body of such loop. Following previous usage for such functions, it's called
  &lt;code&gt;single_step()&lt;/code&gt;. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:9" title="Jump back to footnote 8 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:10"&gt;
&lt;p&gt;Except when you start rendering ferry routes. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:10" title="Jump back to footnote 9 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:11"&gt;
&lt;p&gt;I never measured it :( &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:11" title="Jump back to footnote 10 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:13"&gt;
&lt;p&gt;Seems like &lt;code&gt;nikola&lt;/code&gt; renumbers the footnotes based on which order they are here at the bottom of the source. The
   first note was 0, but it renumbered it and all the rest to start counting from 1. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:13" title="Jump back to footnote 11 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:14"&gt;
&lt;p&gt;Have in account that I'm explicitly making a difference between a non-blocking/&lt;code&gt;select()&lt;/code&gt; loop from an
   &lt;code&gt;async/await&lt;/code&gt; system, but have in account that the latter is actually implemented with the formet. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/#fnref:14" title="Jump back to footnote 12 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>aiohttp</category><category>asyncio</category><category>fastapi</category><category>flask</category><category>openstreetmap</category><category>python</category><category>renderd</category><category>twisted</category><guid>https://www.grulic.org.ar/~mdione/glob/posts/writing-a-tile-server-in-python/</guid><pubDate>Tue, 30 Jul 2024 09:02:53 GMT</pubDate></item><item><title>Sending AWS CloudWatch alarms through SNS to MSTeams</title><link>https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/</link><dc:creator>Marcos Dione</dc:creator><description>&lt;p&gt;&lt;em&gt;I'm new to AWS os please take the following statements with a grain of salt. Also, I'm tired, but I want to get this of
my chest before the weekend begins (although, technically, it has already begun), so it might not be so coherent.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;AWS provides some minimum monitoring of your resources with a tool called CloudWatch. Think of prometheus + grafana, but
more limited. Still, is good enough to the point it makes sense to setup some Alerts on it. Many of AWS's resources are
not processes running on a computer you have access to, so you can't always install some exporters and do the monitoring
yourself.&lt;/p&gt;
&lt;p&gt;If you're like me, CloudWatch Alerts must be sent to the outside world so you can receive them and react. One way to do
this&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; is to channel them through SNS. SNS supports many protocols, most of them internal to AWS, but also HTTP/S. SNS
is a pub-sub system, and requires a little bit of protocol before it works.&lt;/p&gt;
&lt;p&gt;On the other end we&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt; have MSTeams&lt;sup id="fnref:3"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fn:3"&gt;3&lt;/a&gt;&lt;/sup&gt;. MSTeams has many ways of communicating. One is Chat, which is a crappy chat&lt;sup id="fnref:6"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fn:6"&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref:7"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fn:7"&gt;7&lt;/a&gt;&lt;/sup&gt;,
and another is some kind of
mix between a blog and twitter, confusingly called Teams. The idea in a Team is that you can post... Posts? Articles? And
from them you can have an unthreaded converstion. Only Teams have webhooks; Chats do not, so you can't point SNS there.&lt;/p&gt;
&lt;p&gt;If you have read other articles about integrating CloudWatch Alerts or SNS to MSTeams, they will always tell you that
you not only need SNS, but also a Lambda program. Since we already handle gazillion servers, not all of them in AWS, and
one in particular we pay quite cheap for dedicated HW, and also we're trying to slim our AWS bill (who doesn't), I
decided to see if I can build my own bridge between SNS and Teams.&lt;/p&gt;
&lt;p&gt;I already said that SNS has a litte protocol. The idea is that when you create an HTTP/S Subscription in SNS, it will
&lt;code&gt;POST&lt;/code&gt; a first message to the URL you define. This message will have a JSON payload. We're interested in two fields:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SubscriptionConfirmation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"SubscribeURL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What you have to do is get this URL and call it. That way SNS will know the endpoint exists and will associate an ARN to
the Subscription. Otherwise, the Subscription will stay unconfirmed and no messages will be sent to it. Interestingly,
you can't neither edit nor remove Subscriptions (at least not with the web interface), and I read that unconfirmed
Subscriptions will disappear after 3 days or so &lt;sup id="fnref:4"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fn:4"&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;SNS messages are also a JSON payload &lt;code&gt;POST&lt;/code&gt;'ed to the URL. They look like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"Type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Notification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"MessageId"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;uuid1&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"TopicArn"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;arn&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"Subject"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"Message"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"Timestamp"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-19T14:29:54.147Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"SignatureVersion"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"Signature"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cTQUWntlQW5evk/bZ5lkhSdWj2+4oa/4eApdgkcdebegX3Dvwpq786Zi6lZbxGsjof2C+XMt4rV9xM1DBlsVq6tsBQvkfzGBzOvwerZZ7j4Sfy/GTJvtS4L2x/OVUCLleY3ULSCRYX2H1TTTanK44tOU5f8W+8AUz1DKRT+qL+T2fWqmUrPYSK452j/rPZcZaVwZnNaYkroPmJmI4gxjr/37Q6gA8sK+WyC0U91/MDKHpuAmCAXrhgrJIpEX/1t2mNlnlbJpcsR9h05tHJNkQEkPwFY0HFTnyGvTM2DP6Ep7C2z83/OHeVJ6pa7Sn3txVWR5AQC1PF8UbT7zdGJL9Q=="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"SigningCertURL"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-01d088a6f77103d0fe307c0069e40ed6.pem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"UnsubscribeURL"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&amp;amp;SubscriptionArn=&amp;lt;arn&amp;gt;:&amp;lt;uuid2&amp;gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, CloudWatch Alerts sent via SNS are sent in the &lt;code&gt;Message&lt;/code&gt; field. As &lt;code&gt;Message&lt;/code&gt;'s value is a string and the Alert is
encoded as JSON, yes, you guessed it, it's double encoded:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"Message"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{\"AlarmName\":\"foo\",...}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Sigh&lt;/em&gt;. After unwrapping it, it looks like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"AlarmName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"AlarmDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"AWSAccountId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"AlarmConfigurationUpdatedTimestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-18T14:32:17.244+0000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"NewStateValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ALARM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"NewStateReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Threshold Crossed: 1 out of the last 1 datapoints [10.337853107344637 (18/01/24 14:28:00)] was greater than the threshold (10.0) (minimum 1 datapoint for OK -&amp;gt; ALARM transition)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"StateChangeTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-18T14:34:54.103+0000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"Region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EU (Ireland)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"AlarmArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;alarm_arn&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"OldStateValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INSUFFICIENT_DATA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"OKActions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"AlarmActions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;sns_arn&amp;gt;"&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"InsufficientDataActions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"Trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"MetricName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CPUUtilization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"Namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS/EC2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"StatisticType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Statistic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"Statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AVERAGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"Unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"Dimensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;aws_id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InstanceId"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"Period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"EvaluationPeriods"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"DatapointsToAlarm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"ComparisonOperator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"Threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"TreatMissingData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"missing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;"EvaluateLowSampleCountPercentile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The name and description are arbitrary texts you wrote when setting the Alarm and the Subscription. Notice that the
region is not the codename as in &lt;code&gt;eu-west-1&lt;/code&gt; but a supposedly more human readable text. The rest is mostly info about the
Alarm itself. Also notice the &lt;code&gt;Dimensions&lt;/code&gt; field. I don't know what other data comes here (probably the arbitrary fields
and values you can setup in the Alarm), all I can say is that that format (list of dicts with only two fields, one called
&lt;code&gt;name&lt;/code&gt; and the other &lt;code&gt;value&lt;/code&gt;) is possibly the most annoying implementation of a simple dict. I hope they have a reason
for that, besides over engineering.&lt;/p&gt;
&lt;p&gt;Finally, notice that the only info we get here about the source of the alarm is the &lt;code&gt;InstanceId&lt;/code&gt;. As those are random
strings, to me they don't mean anything. Maybe I can setup the Alarm so it also includes the instance'a name&lt;sup id="fnref:5"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fn:5"&gt;5&lt;/a&gt;&lt;/sup&gt;, and
even maybe the URL pointing to the metric's graph.&lt;/p&gt;
&lt;p&gt;Finally, Teams' webhook also expects a JSON payload. I didn't delve much in what you can give to it, I just used the
&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt; and &lt;code&gt;themeColor&lt;/code&gt; fields. At least &lt;code&gt;text&lt;/code&gt; can be written in MarkDown. You get such a webhook going to the Team,
click in the &lt;code&gt;⋮&lt;/code&gt; ("vertical ellipsis") icon, "Connectors", add a webhook and obtain the URL from there. &lt;code&gt;@type&lt;/code&gt; and
&lt;code&gt;@context&lt;/code&gt; I copied from an SNS-to-Lambda-to-Teams post.&lt;/p&gt;
&lt;p&gt;So to build a bridge between CloudWatch Alerts through SNS to MSTeams's Team we just need a quite straightforward
script. I decided to write it in Flask, but I'm pretty sure writing it in plain &lt;code&gt;http.server&lt;/code&gt; and &lt;code&gt;urllib.request&lt;/code&gt;
to avoid dependencies is not much more work; I just didn't want to do it. Maybe I should have tried FastAPI instead;
I simply forgot about it.&lt;/p&gt;
&lt;p&gt;Without further ado, here's the script. I'm running Python 3.8, so I don't have &lt;code&gt;case&lt;/code&gt;/&lt;code&gt;match&lt;/code&gt; yet.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#! /usr/bin/env python3&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;requests&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt; &lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="si"&gt;=}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="n"&gt;request_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="c1"&gt;# python3.8, not case/match yet&lt;/span&gt;
    &lt;span class="n"&gt;message_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Type'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'SubscriptionConfirmation'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'SubscribeURL'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"hello &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'TopicArn'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!"&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s1"&gt;'@type'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'MessageCard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'@context'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'http://schema.org/extensions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'themeColor'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'4200c5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'Notification'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;alarm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Message'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Subject'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Message'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;instance_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Trigger'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'Dimensions'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'value'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'NewStateValue'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'ALARM'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'FF0000'&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'00FF00'&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;instance_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Trigger'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'MetricName'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"""&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'AlarmName'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;​&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Trigger'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'MetricName'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Trigger'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'ComparisonOperator'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Trigger'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'Threshold'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Trigger'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'Period'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; minutes.&lt;/span&gt;
&lt;span class="s2"&gt;​&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'AlarmDescription'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;​&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'NewStateReason'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;​&lt;/span&gt;
&lt;span class="s2"&gt;for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;instance_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; passed to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'StateChangeTime'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;."""&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'themeColor'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://&amp;lt;company&amp;gt;.webhook.office.com/webhookb2/&amp;lt;uuid1&amp;gt;@&amp;lt;uuid2&amp;gt;/IncomingWebhook/&amp;lt;id&amp;gt;/&amp;lt;uuid3&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"OK"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Again, I'm new to AWS. This is how it's setup at &lt;code&gt;$NEW_JOB&lt;/code&gt;, but there might be better ways. If there are, I'm happy
 to hear them. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;'we' as in me and my colleagues. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fnref:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Don't get me started... &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fnref:3" title="Jump back to footnote 3 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;I know all this because right now I have like 5-8 unconfirmed Subscriptions because I had to figure all this out,
  mostly because I couldn't find sample data or, preferably, a tool that already does this. They're 5-8 because you
  can't create a second Subscription to the same URL, so I changed the port for every failed attempt to confirm the
  Subscription. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fnref:4" title="Jump back to footnote 4 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;We don't have pets, but don't quite have cattle either. We have cows we name, and we get a little bit sad when we
  sell them, but we're happy when they invite us to the barbecue. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fnref:5" title="Jump back to footnote 5 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;OK, I already started... &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fnref:6" title="Jump back to footnote 6 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;I added this footnote (I mean, the previous one... but this one too) while reviewing the post before publishing.
  Putting the correct number means editing the whole post, changing each number twice, which is error prone. In
  theory &lt;code&gt;nikola&lt;/code&gt; and/or MarkDown support auto-numbered footnotes, but I never managed to make it work. I used to
  have the same issue with the previous static blog/stite compiler, &lt;code&gt;ikiwiki&lt;/code&gt;, so this is not the first time I have
  out-of-order footnotes. In any case, I feel like they're a quirk that I find cute and somehow defining. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/#fnref:7" title="Jump back to footnote 7 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>aws</category><category>cloud_watch</category><category>flask</category><category>ms_teams</category><category>python</category><category>sns</category><guid>https://www.grulic.org.ar/~mdione/glob/posts/sending-aws-cloudwatch-alarms-through-sns-to-msteams/</guid><pubDate>Fri, 19 Jan 2024 20:36:45 GMT</pubDate></item></channel></rss>