That Which Gave Chase

Released in 2023, played on Linux in 2024.

Mush your dog sled across cruel Arctic wastelands, driven onwards by a brisk and intense companion, who hired you to take him back to some remote spot, where it becomes apparent he had some sort of revelation, or maybe a breakdown.

The low-res, dithered presentation conveys the harsh, blinding conditions, as you struggle to make out details through the relentless wind and ice. Smash cuts disorient you, making the days and nights of the journey blur into one another, leaving only fragmentary, disjointed memories: sledding across the ice; nights in crude wooden huts, collapsing into rough bunks; righting the sled while your companion curses you for a fool; silent moments alone, outside in the dark; mounting the sled before dawn. The sense is of endless, exhausting time spent covering the distance, through punishing conditions, and it's surprisingly evocative.

The narrative leans into the disorientation, making nothing clear. Your companion becomes increasingly cryptic. He urges you onward, never pausing more than absolutely necessary. The deer behave increasingly strangely. Your companion regales you with sickening tales of the investigative mistreatment he subjected them to on his previous visit. By the time the strange mushrooms come into play it is very obvious that you are in a place to which you should never have come, very far from anywhere or anyone, with mounting dread, alone with with a madman. What happened the last time he took this route? What did he leave behind here? What awaits at your journey's end?

Doesn't outstay its welcome, all done in an hour. But the memories remain.

Overhauled Manual for Epomaker Galaxy80 Tri-Mode Keyboard

Loving the new keyboard, an Epomaker Galaxy80 with Feker Marble White switches.

My requirements are pretty much the same as last time I bough a keyboard:

  • Tenkeyless layout, or TKL as it's known, i.e. without a numpad. The kiddo and I fit two side-by-side gaming stations at this desk, and the extra mouse-swiping space is precious, as is the ergonomics of putting the mouse just a few inches closer.
  • Standard ANSI layout, to match the other keyboards I commonly use.
  • Mechanical, although I'm not experienced enough to know a good one from a bad one.
  • At least two connections which are easy to switch between, for work and personal computers. This one has five, three of which are Bluetooth.
  • At least one of those connections should be reasonably low latency, i.e. <5ms, which means wired or a dedicated 2.4GHz dongle, not Bluetooth. The Galaxy80 has both. I'm a long way away from being a pro gamer, but even down here in the GamerDad leagues, I seem to be more aware of annoying latency than most people are.
  • Backlit. I don't especially care about per-key RGB, but that seems to be extremely common. Shine-through keycaps would be nice, but these seem to be increasingly rare outside of garish gamer-boi cyber-monstrosities, so not a big deal.
  • Hot-swappable switches. This is the requirement I compromised on last time I bought a keyboard, settling for the Logitech G915, which was great, but got old after switches started failing. I'm tired of desoldering them and am noping out to buy something else, a mere 16 months later.
  • Not egregiously incompatible with Linux. It would be hard to find a keyboard which doesn't actually work with Linux, but maybe some manufacturer has buried some vital configuration detail in badly written Windows-only configuration software that doesn't play nice with Wine, etc.
  • Without expensive features I don't need, like configurable activation height, or OLED screens.

The switches are described as sounding "like marbles clacking", which worried me that they might be too loud and piercing. But now it's arrived, they are actually quieter than any other mechanical switch I've had. The sound is deeper than I expected. Recognizably like marbles, but merged with the sound of pebbles, and a hint of a wooden xylophone.

I really like it! Although since pulling the trigger I've seen that Reddit doesn't like Epomaker. I'm just not going to read those posts for now.

As seems traditional with all keyboards, the manual is quite hard to read. Here's my overhauled manual, for future reference.

Update: The incantations needed to get function keys working the way you want them to on Ubuntu :eyeroll: etc. Despite the mention of 'apple' in here, this still assumes you have the keyboard switched into 'Win' mode. (via Reddit):

echo "options hid_apple fnmode=2" | sudo tee /etc/modprobe.d/hid_apple.conf
sudo update-initramfs -u

and reboot.


TIL: Constructing a PDF from .jpg image files

I have some folders of .jpg images that make up a comic. I want to convert them into a PDF to read on my tab and other devices, and import into by bookshelf in Calibre.

1. Install some prerequisites

sudo apt install imagemagick pdftk

2. Do the conversion

The versatile ImageMagick has a 'convert' command that seems to handle it:

convert *.jpg output.pdf

But this has some issues:

2.1. Failure due to security policy

'convert' currently refuses to generate PDFs: 'attempt to perform an operation not allowed by the security policy'. Apply the fix described on StackOverflow. :eyeroll:

2.2. Failure due to cache space

You might not need this fix if you generate smaller documents, or generate chapter-by-chapter as described below, but here it is in case.

Don't close that editor! In the same policy.xml you were just editing are resource size declarations for memory and disk. If 'convert' barfs with an error about running out of cache space, then bump up the disk resource size. I set mine to 8GB. StackOverflow again for details. :eyeroll: again.

3. Include a table of contents

I want to add bookmarks to the generated PDF marking each chapter.

Put the .jpgs into subdirectories by chapter, eg:

src/
|--chapter01/
|  |--0001.jpg
|  |--0002.jpg
|  |  ...
|--chapter02/
|  |--0001.jpg
|  |--0002.jpg
|  |  ...
|
...

Pad the chapter numbers with preceding zeros so that they sort into the correct order. I added an artificial 'chapter00' containing the front cover, separate from individual chapters.

Now we need to generate individual PDFs for each chapter. We can then use 'pdftk' to count the number of pages in each chapter, and use those counts to place bookmarks on the correct pages when pfdtk combines the chapters into one final output PDF.

I ended up regenerating each chapter a bunch while I tweaked the content, such as deleting adverts from the images. So I put these commands into a Makefile:

help: ## Show this help.
    @grep -E '^[^_][a-zA-Z_\/\.%-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}'
.PHONY: help

chapter_dirs=$(wildcard src/*)
chapters=$(chapter_dirs:src/%=%)
chapter_pdfs=$(chapters:%=%.pdf)
bookmarks=bookmarks.txt
output=output.pdf

clean: ## Delete all generated PDFs
    rm -f $(chapter_pdfs) $(output)
.PHONY: clean

chapter%.pdf: src/chapter%/*.jpg ## Each individual chapter, use 2 digits
    convert src/chapter$*/*.jpg $@

$(bookmarks): $(chapter_pdfs)
    ./make-bookmarks >$(bookmarks)

$(output): $(chapter_pdfs) $(bookmarks)
    pdftk $(chapter_pdfs) cat output - | \
    pdftk - update_info "$(bookmarks)" output "$(output)"

all: $(output) ## Build final output PDF
.PHONY: all

Where 'make-bookmarks' is a bash script that generates the intermediate 'bookmarks.txt' file:

#!/usr/bin/env bash

set -e # exit on error
set -u # treat unset vars as errors
# set -x # debugging output
set -o pipefail

# Generate a bookmarks file for all the matching PDF files

fmt="BookmarkBegin
BookmarkTitle: %s
BookmarkLevel: 1
BookmarkPageNumber: %d
"

declare -a files=(chapter*.pdf)
page=1
for file in "${files[@]}"; do
    title="${file%.*}"
    printf "$fmt" "$title" "$page"
    num_pages="$(pdftk "$file" dump_data | grep NumberOfPages | awk '{print $2}')"
    page=$((page + num_pages))
done

Now make all will produce the final output.pdf. You might want to open up the generated bookmarks.txt and edit the placeholder "chapter01" names. Then run make all again to regenerate the final output PDF with your fixed chapter names.

Rorschach II meets Adrian

TIL: Shell environment variable tricks

envsubst is an executable you likely already have on your PATH (part of the gettext package, often installed with dev packages), which is a convenient way to replace $VAR or ${VAR} style environment variables with their values. This allows rendering templates without heavyweight tools like Ansible, Jinja, or embedding with heredocs. Usage is:

envsubstr <template >rendered

For example:

$ envsubstr <<<"Hello $USER"
Hello jonathan

If you'd like to use KEY=value declarations from a dotenv-style .env file, you can auto-export them by setting the -a Bash option:

set -a; source .env; set +a

Something I've managed to avoid ever realizing for 30 years, but now that I've seen it I can't imagine a week going by without using it. The kind of thing that should be part of everyone's "Week 1 in a terminal" training that formal education courses never include.

Ferris Bueller's Day Off

Ben Stein in Ferris Bueller's Day Off

Directed and written by John Hughes, 1986. IMDB

So, way back in my teen years, we had a VHS tape of this, which friends and I played and played and played, probably racking up more rewatches than any other movie in my life. So it was a pleasure to break it out for our 11 year-old, some some 37 years later (!), to see whether it still holds up, and find that it really does.

By chance this was the week after we'd just watched The Blues Brothers, so we got to compare and contrast two movies set in Chicago - a privileged white story, and a poverty stricken, largely colored one, which even share scenes filmed in the very same restaurant.

Back then, I had no idea who Ben Stein was, so it was amusing to see him now and suddenly join the dots. Apparently his infamous "voodoo economics" speech had no script and was ad-libbed.

Reviewing Rooney's comical attempts to break into the Buellers' house made me realize for the first time that this was Hughes' dry-run at what would become Home Alone.

I had always been frustrated that I'd never been able to lay my hands on the "You're not dying" song that Cameron plays while sick in his bedroom (i.e. here's the few seconds of it on Youtube, exactly as it appears in the movie.)

Now we have the Internet, I can see that this failure wasn't exactly my fault - there is no such song. The few bars we hear were whipped up by Ira Newborn specially for the film, based on an old Louis Armstrong song, Let My People Go. Fortunately for us, one man was obsessed about it enough to actually recreate a full length song based on the snippets from the movie. Here is Daniel Simone's Let My Cameron Go, full of a lush Pink Floyd sound, and ripe with the sort of ecstatic anticipation that even Roger Waters would be proud of.

Duly added to my rotation for next time I'm sick.



Resolution and The Endless

Resolution

Resolution (2012)

The Endless

The Endless (2017)

I only had a hazy awareness of Justin Benson and Aaron Moorhead, the writer and directors, before watching these movies. But having discovered them, I now realize that they are doing just about my favorite thing in film: Quirky, intense, psychological drama wound around some high concept science fiction.

Going in, I hadn't realized the two films are related. But then they contain the same scene, viewed from two different angles (pictured above), and it starts to become clearer. As it happens, I watched them out of order - my enthusiasm for The Endless caused me to look up their earlier Resolution. But with hindsight, I think this is actually the best order to view them. If Resolution has a weakness, it's that the science fictional elements seem a bit arbitrary. Why should this supernatural entity focus its narrative-obsessed attentions on these two men, here in this cabin, out in the middle of nowhere? But in The Endless, this particular brand of supernatural outlandishness is revealed to be just part of a wider pattern, affecting many people in this geographical area. Although this is the bigger, weirder story, it is more fully fleshed out and becomes more believable, creating a setting which recontextualizes and improves the earlier film.

Rating: 10/10 if you like mindbending SF horror, 0/10 if you prefer something a bit more polished and comfortable.


Aurora

Aurora cover

by Kim Stanley Robinson, 2015

It has long been held by fans of science fiction that fantasy is a lowly subset of science-fiction, or perhaps a disreputable cousin, one for whom the normal rules of discernment do not apply. If such unlikely and unrealistic things as dragons and magic are allowed, the reasoning goes, then the book cannot be relied upon to deliver any kind of coherent narrative experience, since the lapsed rule-set now allows for any old ex machina plot twists to save the day. A magical "defeat the evil" spell? No problem. A new mythical creature capable of defeating the previously unassailable one? Why not? All reason is gone.

It's more useful though, is to invert the hierarchy of this received wisdom, and consider science fiction as a subset of fantasy. Mentioning this in fandom circles blows mental fuses. Does not compute. But the speculative flights of science fiction are also fantasies. Just fantasies that a particular type of person finds especially beguiling, compelling, and believable. To some extent, I concede that on occasion they are believable because they seem to be a reasonable extrapolation of our current situation. But no matter how reasonable your extrapolation seems to be, it's always possible that reality will zig instead of zag, and even the most humdrum tale of a rocket man's life will find itself at odds with the unexpected reality of suspended human spaceflight in the face of spiraling real-world costs. The vision that one is selling is always, to a greater or lesser extent, a wishful one - a fantasy.

This becomes immediately apparent once we stray beyond the confines of low Earth orbit, to take in the wider scope of science fiction, the vast majority of which encompasses tales across the galaxy, nay, the universe, including time travel, teleportation booths, aliens of every color, quantum reality displacement, and multiversal escapades in which literally everything is possible. These are very clearly fantasies, and it is intensely curious to me why this sort of fantasy is considered more "realistic" or "believable" than, say, flying lizards with fiery breath. Even though the narrative hand-waving that explains away the former - "It's an alternate universe, where different rules apply" - is abundantly adequate to more than completely explain anything in the fantasy realm.

I once asked my guru science fiction critic Damien Walter what makes people consider some stories believable, while other are not. He replied with a statement that has stuck with me ever since: People are willing to invest the effort to provide the conceptual scaffolding around an idea to make it seem believable (e.g. to speculate on the mechanism that might allow for a faster-than-light hyper-drive) when the story fulfills some deeper psychological need for them.

Hence, a story on the same topic as Aurora, of a generation ship sent to colonize an Earth-like planet orbiting the nearby star of Tau Ceti, is (usually) a story about the triumph of modernism. Such stories leverage the sources of strength in the modern world, science and technology and colonialism, and a reader who is invested in a modern world-view will feel validated and empowered by this type of fantasy. They will be will be willing to exercise whatever extracurricular creative effort is required on the part of the reader to make the story believable. Doing so will inspire them with the feeling that their world all makes sense, is leading to something, so that their daily grind is a part of the heroic story of how humanity transcends its planetary origins. This is much more fulfilling than investing any effort getting on board with the waning powers of superstition that are represented by the fantasy genre.

spoilers

In keeping with this, Aurora's colonists are granted every conceivable boon that science and industry can supply. A ship fully ten kilometers across, enclosing twenty four massive biomes, each stuffed full of hills and lakes, soil and forests, microbes and wildlife. A population of well over a thousand human beings. Miraculous nanotech fabricators, and megatons of elemental feedstock to run them. A miraculous acceleration laser, fired from Titan for decades after departure, allowing the ship to coast up to 0.1c, making the journey in only seven generations, while retaining enough fuel to decelerate for arrival. A miraculous magnetic shield protects the ship from catastrophic collisions with stray particles along the way. A benign AI runs the ship, amusingly pressed into service as the narrator of the tale, and grows visibly more sentient, emotionally robust and capable as the years pass.

And hot damn, they are going to need all these things, because in this story, human interstellar colonization is revealed for the fantasy it really is. Nothing works out, and the problems encountered are far bigger than anything the ship's designers planned for. Although the ship does limp into orbit around the destination planet, soon after that people start dying, major disagreements emerge which descend into catastrophic riots, and the ship's society falls apart - when have humans ever invented a reliable form of governance?

There is a rip-roaring final act, that stretched my credulity, but revives the stakes and entertainment value in what might otherwise be a relentless downer of a read.

The book, and interviews with the author, caused quite a stir in science fiction circles. People were extremely angry. The book was attacking their deeply held beliefs that the future of humanity is as a successfull space-faring species. They had invested their identity in this world-view, because of how it serviced their psychological needs for fulfillment and meaning. They had developed a religious conviction around this particular kind of fantasy.

The point of Aurora is to highlight the idea of human interstellar colonization as a dangerous distraction from the very real project of taking care of the long term health of our planet and our society right here on Earth. It is going to take beyond miraculous levels of technology and resources to start thinking about interstellar travel. If, by some miracle, we make it to a year 10,000 utopia, with infinite resources and the wisdom to manage them, then sure, we can worry about interstellar travel. But for now, can we just focus on some of the very basic problems of existence here on Earth, like how to make everyone fed and liberated, educated and fulfilled, without killing our planet to do it? Maybe invent some sort of government that is reliably able to do that? That would be nice.


Fully Operational

Now witness the power of this fully armed and operational battle station.

My desk featuring too many computers

New job means new laptop means it's time to clean and re-org the desk.

Leftmost blue skies
Linux laptop (a free hand-me-down from a job ten years ago). Acting as the house Plex / streaming media server, usually tucked away more discreetly than this.
Left top green forest
Heavy duty work / gaming Linux laptop ("hardware bonus" from my last employer). Has been my primary work machine, but sounds like it's getting replaced by...
Left bottom spaceship drawing
Macbook Pro (Brand new! Just unwrapped yesterday. Thank you new employer Lambda!) Looks like this means I'm returning to developing on a Mac and VMs, after a full decade on Ubuntu & derivatives. I'm told Docker for Desktop now behaves better than it used to.
Left bottom, under the Mac
You can sort of see the 10" whiteboard I use to combat ADHD by writing a sentence about what I'm supposed to be working on, then I can spot it every few minutes and drag my mind back to the task in hand. (a technique described in the fabulous Self Command by Chris DeLeon.
Center
Main monitor and wireless tenkeyless mechanical keyboard & mouse combo, all switchable to any of the laptops. Under the keyboard you can sort-of see the Magic the Gathering 13x24" gaming mat (free from local gaming store's MtG lessons) pressed into duty as the world's most gigantic, beautiful, and luxurious mouse mat.
Right monitor, keyboard and mouse
are wired to the Windows gaming PC under the desk (not visible). The kiddo's current Astroneer session is visible. The monitor is switchable to any of the laptops.
Right tab
Absolute workhorse 12.6" Android tablet on which I do most of my reading, laid in the picture here just to be gratuitous.

Illustrating Uses of IBM Cloud Security Groups

I wrote this high-level public-facing guide while employed by IBM, creating the security groups feature for IBM Cloud. It used to reside on the IBM blog, but has recently been replaced by newer content, so I've preserved it here for posterity.

This article illustrates a few possible uses of IBM Cloud Security Groups, a per-instance firewall for IBM Cloud virtual instances.

Why Use Security Groups?

Security groups firewall your IBM Cloud applications from nefarious network traffic, protecting you and your company from the efforts of “industrious users” trying to bring down your application, or make off with your customer's credit card details. If those sound like sub-optimal outcomes for your situation, read on…

Allow Incoming SSH Connections

The simplest use of security groups is to allow a single type of network connection to your instances, blocking all other traffic. For example, to allow only incoming SSH connections, which are TCP connections on port 22. All other types of traffic, such as ICMP ‘ping' connections, or TCP connections on other ports, are blocked. Fig 1. A security group configured to allow incoming SSH

A diagram representing SSH connections being allowed from support engineer to instances within security group 1, while ping connections are denied

Fig 1. A security group configured to allow incoming SSH.

When instances 1 & 2 are added to your security group, firewalls are created directly on those instances,configured to allow or deny the corresponding traffic. Hence, your support engineer can create SSH connections, but cannot send arbitrary network traffic.

Allow SSH from a Specified IP Address

The above scenario allows SSH connection attempts from any IP address. To increase security, you might only allow connections from a particular instance. Fig 2. A security group configured to allow incoming SSH connections (TCP port 22) from a particular IP address.

A diagram representing SSH conections being allowed from a known IP address to instances within security group 1, while connections from a disgruntled employee at a different IP address are denied

Fig 2. A security group configured to allow incoming SSH connections (TCP port 22) from a particular IP address.

The security group has been configured with the IP address used by a support engineer – the single instance that is authorized to make a connection. Connections from other instances are blocked.

As well as allowing traffic from a single IP address, security groups can be configured with a CIDR block, to allow traffic from all instances on that subnet. Fig 3. A security group configured to allow incoming SSH connections (TCP port 22) from all instances on a given subnet.

A diagram representing SSH conections being allowed from a known subnet to instances within security group 1, while connections from a hacker at an IP address ouside the subet are denied

Fig 3. A security group configured to allow incoming SSH connections (TCP port 22) from all instances on a given subnet.

The diagram shows instances on an authorized subnet (deploy1 & deploy2), representing a project CI/CD infrastructure, all being able to make SSH connections to our protected instances, for example to deploy updates to our application. Other instances, such as an enterprising hacker, are blocked.

Allow Application Instances to Access a Distributed Data Store

Another use case would be to allow application servers to access the nodes of a distributed data store. To do this, we'll make a few changes to the above security group configuration.

Firstly, we modify the open port from SSH's 22, to MongoDB's default query API port of 27017.

Secondly, security group 1 is now allowing the creation of outgoing network connections, from our application instances, where previously it was allowing incoming connections. Security group rules can manage traffic in either direction.

Thirdly, we don't want our data store instances to be unprotected, so we'll put them into a security group of their own. Since there's now two security groups, we'll give them names: “app” and “db”.

For now, security group “db” allows all incoming MongoDB queries (TCP connections on 27017), without restricting the IP addresses allowed to make connections. We'll fix that soon. Fig 4. Two security groups configured to allow application servers to send queries to MongoDB nodes on a subnet.

A diagram representing queries from instances in security group "app" being allowed to connect to port 27017 of the subnet of instances in security group "db", which similarly allows incoming connections on TCP port 27017

Fig 4. Two security groups configured to allow application servers to send queries to MongoDB nodes on a subnet.

For clarity, these diagrams don't show the many connections which are blocked by this setup - which are, of course, the whole point of security groups. On the above diagram, blocked connections would include:

  • On app instances:

    • All incoming connections.
    • Outgoing connections that aren't TCP, or are on the wrong port.
    • Outgoing connections to anything other than a DB instance.
  • On DB instances:

    • All outgoing connections.
    • Incoming connections that aren't TCP, or are on the wrong port.

This setup does have a couple of problems. It relies on our MongoDB instances all residing on a single subnet. Worse, as mentioned earlier, it allows any IP address in the world to make queries to MongoDB. We'll fix both of these next.

Using Remote Groups to Specify Arbitrary IP Addresses

Specifying allowed instances using a CIDR block can be inappropriate. It's often preferable to specify a set of arbitrary IP addresses instead. We can do this by using a second security group – known as a remote group – to contain the set of allowed instances. Our first security group can then refer to the remote group to specify which instances are allowed.

In our example above, we would modify the “app” security group by dropping the CIDR block, and replacing it with a reference to “db” as a remote security group.

Similarly, we would modify the “db” group to use “app” as a remote group, only allowing connections from the members of that group.

A diagram representing instances in security group "app" being allowed to connect to instances in security group "db", via port 27017 only

Fig 5. Two security groups configured to allow application servers to send queries to MongoDB nodes using remote groups.

This has several advantages. Firstly, our data store no longer accepts malicious queries from hackers all over the internet – only from our app instances.

Secondly, our data store instances no longer need occupy a subnet, they can have arbitrary IP addresses.

Because the security groups now specify allowed hosts by referencing each other, when the members of either group changes, the instance level firewalling rules on all instances are updated automatically, to allow or deny traffic based on the new membership.

This configuration starts to show what makes security groups a flexible, dynamic, low-maintenance solution.

Accepting Web Requests

Our application instances aren't any use without a web front end. We put our web instances into their own security group (“web”), which allows incoming requests from users on the web, and outgoing API requests to our app instances, on port 61516.

Similarly, we need to add a second rule to the “app” security group, to allow incoming requests from “web”. Fig 6. A traditional three-tier application using remote security groups.

A diagram representing a user connecting via port 80 to instances in security group "web", which connect to instances in group "app" via port 61516, which connect to group "db" via port 27017

Fig 6. A traditional three-tier application using remote security groups.

The more types of server we add to our setup, the more benefit “remote” groups provide, by minimizing setup configuration, and by automatically keeping firewalling rules up to date when group membership changes.

A future blog post will discuss how to set up this three-tier scenario, and describe details such as how to use multiple network interfaces on an instance, such as the web instances which will use a public IP address exposed to users, versus a private IP exposed to the API instances.

Add a Bastion

We need some way to access our servers, so that we can, for example, deploy new versions of our application. Commonly this is achieved using a bastion server, which provides a single, carefully hardened point of access.

Here we add a bastion server, and modify all our security groups to allow SSH access from the bastion to all our instances. The bastion instance itself would be configured to only allow incoming connections from the appropriate points in a CI/CD infrastructure (not shown.) Fig 7. Adding a bastion server with SSH access to all other instances.

A diagram representing the same elements as figure 6 above, with the addition of a "bastion" group which connects to all instances via SSH on port 22

Fig 7. Adding a bastion server with SSH access to all other instances.

This is starting to look like a realistic setup for a modest but scalable multi-tier application.

Conclusion

Security groups are a flexible and powerful way to firewall network traffic to and from your system's instances. We've shown how they might be used in a few typical scenarios, and hopefully demonstrated that they are flexible enough to accommodate many others. For more information, see Getting Started with Security Groups.


Jonathan Hartley's smiley face
Jonathan Hartley
Senior Cloud Developer