golden-demise

I've been running my own Fediverse instance for a few months now, and have some data to share around costs and resource utilization.

If you're just getting interested in the Fediverse, hosting your own server is typically not the best way to get started. You'll want to try out some of the existing servers and technologies to understand the type of community you wish to participate in, and how you prefer to interact with the broader Fediverse. This post will assume you've already done your research and have decided to start your own Pleroma server (a popular alternative is Mastodon).

Pleroma is known for being lightweight and fast- it also requires few resources to host. Frequently, a figure of 10 USD per month is used as a general rule of thumb, but if you're willing to utilize budget VPS providers(I personally use racknerd and have been pleased with them) you can come in significantly under this figure. At the time of this writing (even in February), you can get a 4GB KVM VPS for under $44 USD per year(or $3.67/month) under the 'Black Friday 2021' category of the store page- this is the size I used for my own instance, and it has yet to exceed utilizing 1/3 of that. For my 2 user instance, I could probably have gotten away with the 2.5GB VKM VPS (27.88/year), but I wanted to account for potential spikes in utilization (as I wasn't familiar with the application's behavior) and additional active users.

In order to measure resource utilization, I decided to utilize the New Relic Infrastructure agent– I chose them simply because their service is free, and the agent is open source. It’s critical to monitor resource utilization for your instance to keep things running smoothly for you and your users, and to identify anomalous patterns before they create problems.

After several weeks of active use, I found that a basic Pleroma (Soapbox-be specifically) installation required little disk space:

root@:~# df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        24G  7.0G   16G  31% /

With this small volume, my disk utilization increased 6.5% in 2 weeks. The increase was totally linear over that timeframe. I’ll need to increase the size of my root volume in the near-ish future if this increase remains linear.

The Memory utilization was also extremely linear, ranging from 1.29G to 1.32G at peak.

CPU utilization did not exceed 10% over the same 2 weeks, and disk IOPS and network traffic have remained trivial.

The barrier to entry for hosting a Pleroma instance is pretty trivial. It requires few resources, sub zero tech knowledge, and is cheap.

The Fediverse is a very unique place with a vast array of software offerings.

My present favorite option for Fediverse software is Soapbox. It adds features at a relatively rapid pace, utilizing an actively developed fork of Pleroma on the backend to facilitate the addition of features such as quote posts that aren't typically available on other fediverse platforms.

The instructions for installation of Soapbox are available here: https://soapbox.pub/install/

If you would like access to the newest features and improvements made to Soapbox, you can switch to the develop branch (some features of the develop branch frontend require the develop branch backend to function, so both need to be upgraded). While the develop branch is still in the testing phase and provided 'as-is' without any warranty, many of the largest and most active Fediverse Soapbox instances utilize it without issue(and I personally strongly prefer it from a performance standpoint).

You can upgrade to the develop branch by running the following commands on an existing built-from-source Soapbox install that uses asdf to manage Erlang/Elixir versions(you can switch the the asdf managed package from repo packages via the instructions here):

cd /opt/pleroma
sudo -Hu pleroma bash
git remote set-url origin https://gitlab.com/soapbox-pub/soapbox.git
git fetch origin --tags
git checkout develop

It is likely that you will need to update your toolchain via running asdf install as the Pleroma user. This may install new plugin versions- in such a case, you'll see a message similar to the below:

Erlang 24.1.6 has been installed. Activate globally with:

    asdf global erlang 24.1.6

Activate locally in the current folder with:

    asdf local erlang 24.1.6

Make sure to activate the new plugin versions prior to running additional commands. The following commands will recompile Soapbox BE:

mix local.hex --force
mix local.rebar --force
mix deps.get
MIX_ENV=prod mix compile

If you started with a Pleroma version prior to 2.3, the database will also require migrating:

MIX_ENV=prod mix ecto.migrate

It's not likely that the systemd service file will change frequently, but it's best practice to copy over the most recent version pulled via:

cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service
systemctl enable --now pleroma.service

Once the Pleroma service restarts, you can then install(the process is also identical for updating) the develop branch for the frontend, which is pretty trivial to accomplish. First, run:

curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/develop/download?job=build-production -o soapbox-fe.zip Then, unzip the new front end into place via:

busybox unzip soapbox-fe.zip -o -d /opt/pleroma/instance

At this stage, you should be able to view your timeline successfully and enjoy the improvements of the develop branch.

Learning how to make Javascript perform tasks asynchronously is important for dealing with many routine tasks when writing applications.

Consider the following function code that performs a dns lookup, which doesn't behave as expected when called:

 const urlResolves = passedUrl => {
  let result = 'default';
  dns.lookup(passedUrl, (err, address, family) => {
    if(err){ 
      console.log('error in resolution');
      console.log(err);
      result = false;
      } else {
        console.log(`address is ${address}, family is IPv${family}`);
        result = true;
      }
  });
  console.log(`the result is: ${result}`);
  return result;
}

When I pass a valid hostname to this function, the result returned will almost always be default. However, the message from the successful block will also print the address and family of the passed URL.

This occurs because the function returns before the DNS resolution has completed(and the value of the result variable is modified). In order to sort this behavior, there are a few primary approaches. In this first short post, I'll briefly discuss Callbacks and demonstrate how to perform a dns lookup using them.

This method for dealing with async calls one method on completion of a function, effectively chaining them.

Here is a version of the urlResolves function that takes a callback:

 const urlResolves = (passedUrl, callback) => {
  let result = 'default';
    dns.lookup(passedUrl, (err, address, family) => {
    if(err){ 
      console.log(`error in resolution of ${passedUrl}`);
      result = false;
      callback(err, result);
      } else {
        console.log(`address is ${address}, family is IPv${family}`);
      result = true;
      callback(null, result);
      }
    });
};

You can then call this function and pass an anonymous function as the callback, returning any error as the first argument to the current function's callback (per node convention).

urlResolves(providedUrl, (err, bool) => {
    if(err){
       res.json({error: 'invalid url'});
       return;
    } else {
      console.log('returning json from dns lookup');
      res.json({original_url: req.body.url, short_url: 'placeholder'});
      return;
    }
  });

While useful for solving trivial problems, using callbacks doesn't scale effectively and creates headaches, particularly as you may find that you have to nest them(this is called 'the pyramid of doom').

A 'Promise' is a better option for accomplishing the same thing. A Promise object either results in a success(which is handled by the Javascript callback resolve) or a failure (which is handled by the reject callback). Whatever is passed to the resolve callback will be passed as a parameter in functions chained via then. Here's a function that performs DNS lookups using this approach- it returns a promise, and the address is then passed to the function chained with then.

const doesResolve = url => {
        return new Promise((resolve, reject)=> {
                dns.lookup(url, (err, address, family) => {
                if(err){
                        console.log(err);
                        reject(err);
                } else {
                console.log('lookup successful');
                resolve(address);
                }
                });
        });
}

I wrote a simple wrapper function for console.log to demonstrate the order in which the functions are executed, and then called it following doesResolve:

const logThis = thingToLog => {
        console.log('Initiating logThis function');
        console.log(thingToLog);
}


doesResolve('www.google.com')
        .then(result => logThis(result));

You can chain additional functions with .then, each of which will pass the value returned as a parameter to the next function. For example:

doesResolve('www.google.com')
        .then(result => result + " first function")
        .then(result => result + " second function")
        .then(result => logThis(result)); //logs "142.251.33.68 first function second function"

You can also return an additional Promise if the additional handlers need to wait.

Later, I'll be editing this post with the addition of Async/Await from ES7.

After coming back to some exercises on exercism involving closures, I quickly learned that my fundamental understanding of them was not what I'd believed. Whether this is due to a month passing since I visited the subject or the concussion I sustained in a recent car accident, I don't know.

In any case, I'm going to write (with likely overlap from previous entries) some very fundamental Closures examples, in the hopes to both document this and reference it later.

Consider the following code:

let runningTotal = 0;
const addToTotal = (num1) => {runningTotal += num1};

const funcBuilder = (unaryFunc) => {
     return (arg) => {unaryFunc(arg)}
    }

const addThem = funcBuilder(addToTotal);

addThem(6); //6
addThem(2); //8

In this simple example, a given number is added to runningTotal via an anonymous function returned by funcBuilder(). This doesn't make use of closures. If you have multiple totals to track, this approach isn't effective, as each successive call will still use the original runningTotal.

const addThem2 = funcBuilder(addToTotal);
addThem2(4); //12, added to existing runningTotal

You can seen an example pulled from the javascript.info tutorial on closure/Variable scope that illustrates use of closure to instantiate/track separate running totals:

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();
let counter2 = makeCounter();

counter and counter2 will increment independent instances of the count variable. The count variables in the above example will be tracked by Javascript, and not cleaned up until all instances of the returned anonymous functions no longer reference this variable.

In my time helping people troubleshoot Sentinel, a very large portion of issues can be traced back to a single concept:

computed or known after apply values.

This is a simple concept in theory- say that you wanted to perform a Sentinel policy check against a newly created AWS instance via it's instance ID- this wouldn't be possible, as Sentinel takes place between the Terraform plan and apply phases. There's no way of knowing what a given instance ID is going to be until after the AWS API call that Terraform makes is complete(after the apply).

This behavior is pretty straightforward- if you can't see the value before the apply, you can't easily use Sentinel(at least tfplan) to check them.

Whether a value is computed depends on how the resource in the associated provider is written.

However, it can be complex at times, as you may be operating on a static item (such as IAM policy) that may use computed values within. If any portion of an given item is computed, Sentinel treats the entire thing as computed.

As such, it's best to break things up into individual items, which reduces the chance that a given piece of that item is computed. For IAM policies, you can use the aws_iam_policy_document data source.

Because of a quirk of the behavior of terraform show -json(which generates the data that Sentinel policies run against), data sources without computed values appear in tfstate/v2 and not tfplan/v2– you'll need the associated import to effectively operate on them.

In Sentinel mocks, you can check for this by reviewing after_unknown, which contains a boolean indicating whether a given value is computed/known after apply.

The data source present in the mocks also contains this field, which will tell you the intended behavior of the value.

There are only a couple of realistic ways to deal with these values in Sentinel- either use tfconfig to operate against the Terraform configuration itself (this is fragile, error-prone, and might not even work), or use tfstate to check AFTER the run is completed to flag violating resources for developers to fix later.

It's fairly straightforward to write your own custom error types in Javascript. The most basic syntax looks something like this:

class ArgumentError extends Error {
  constructor() {
    super(`The sensor is broken. Null temperature value detected.`);
  }
}

It's important to first understand the workings of the actual Error prototype, along with Javascript prototypal inheritance.

A Javascript class is a template for objects- it will always have a constructor method, which is automatically executed when a new instance of this class is created. If you don't include one, Javascript will include one for you.

The extends statement allows your object to access all of the methods and properties of the parent method.

In this context, it means that ArgumentError has access to all of the contents of Error. However, to actually access those contents you need to use the super function. The super function allows you to call the parent class constructor with the arguments provided, or call functions on the object's parent. In this context, the message about the sensor being broken is passed into super, which calls the parent Error constructor passing the message as an argument. The Error constructor takes a few optional parameters- providing the string will pass this as the message.

Effectively, this means that ArgumentError is simply an Error that is passed a specific message.

You can also pass a specific argument in a trivially more complex example:

export class OverheatingError extends Error {
  constructor(temperature) {
    super(`The temperature is ${temperature} ! Overheating !`);
    this.temperature = temperature;
  }
}

You can then perform checks against the property of the error, calling functions accordingly.

try {
    actions.check();
  } catch (error) {
    if(error instanceof ArgumentError) {
      actions.alertDeadSensor();
    } else if(error instanceof OverheatingError && error.temperature > 650) {
      actions.shutdown();
}

Writing functions that are nested in a functional style in JavaScript can be tricky. For instance, consider the following code:

const composeu = (unaryFunc1, unaryFunc2) => {
    return function(arg) {
        return unaryFunc2(unaryFunc1(arg));             
    };
};

In order for this to work properly, the nested function invocations need to be written inside out. Existing functions can be effectively strung together/piped in a UNIX like fashion. The spread operator (...) allows for the number of functions chained to be variable.

In the following similar example, a function calling two binary functions are called on a set of arguments (known length) is returned:

const composeb = (binFunc1, binFunc2) => {
    return function(arg1, arg2, arg3) {
        return binFunc2(binFunc1(arg1,arg2), arg3);
    }
}

You can also use these variables to control function flow, such as by storing a local variable. I wasn't able to figure out the following problem initially:

// Write a `limit` function that allows a binary function to be called a limited number of times

const limit = (binFunc, count) => {
    return function (a, b) {
        if (count >= 1) {
            count -= 1;
            return binFunc(a, b);
        }
        return undefined;
    };
}

In my line of work, I frequently end up helping customers who are running into issues with implementing Hashicorp Sentinel policies.

It's a “policy as code” product that ties in nicely with the Infrastructure as Code nature of Terraform. For additional information around the philosophical approach behind Sentinel and the advantages it confers, I recommend seeing this post from one of Hashicorp's founders, Armon Dadgar:

https://www.hashicorp.com/resources/introduction-sentinel-compliance-policy-as-code

Sentinel is being revised very rapidly and is a paid product, so finding code examples that both actually work and are current can be very tricky. One of the best places to start is this repository of example Sentinel policies(and helper functions) for various cloud providers:

https://github.com/hashicorp/terraform-guides/tree/master/governance/third-generation

Though Hashicorp literature states “Sentinel is meant to be a very high-level easy to learn programming language”, it isn't easy, particularly if you aren't familiar with the general syntax of go. The difficulty extends outside the realm of the syntax to the actual way that troubleshooting is implemented, and the lack of IDE tooling (outside of a VSCode syntax highlighter). Debugging is chiefly a matter of using print and then running the sentinel binary with the trace flag, as error messages are often quite opaque.

For example, say you're creating a policy that is meant to check for tags, and you unexpectedly run into a situation where undefined is being returned where it's not being expected. This is typically the result of unexpected provider configuration, such as the addition of aliases.

Analyzing this can require a mixture of tfplan, tfconfig, and even tfstate if data sources therein don't contain computed values. Understanding computed values is critical to effectively writing Sentinel code- a lot of resources have values that aren't known until after an apply is performed. Because Sentinel runs occur between the plan and apply phases, it's not possible for a policy to effectively operate against such values. If your Sentinel mocks contain unknown for 'after' the value is likely computed.

If you're using the helper functions from the linked Hashicorp repository, these will often require some combination of all three imports.

At present, the only way to iterate over provider aliases is to use tfconfig.providers, which returns a JSON object containing specified providers.

Recursion has always been a difficult concept for me to wrap my head around. Consequently, Closure in Javascript is also difficult to understand. Here's a brief series of exercises on Front End Masters, written here mostly to organize my thoughts and try to cement the concepts I've learned.

Consider the following function that takes an argument, and returns a function that returns that argument:

const identityf = arg => {
    return function(){
        return arg
    }
}

This is possible because of Closure, in which the context of an inner function includes the scope of the outer function. Nested functions can see block variables. On the back end, this involves using the heap instead of the stack to allow child functions to operate once the parent function is exited.

Things get more complex when you return functions:

// A function that takes a binary function, and makes it callable with two invocations
// For instance, calling liftf(multiply) (5) (6) would return 30
const liftf = func => {
    return function (first) {
        return function (second) {
            return func(first, second);
        };
    };
}

The reason that the multiple invocations(the (5) and (6) in the comments above) are possible is that the function is itself returning functions, and subsequent invocations are passed as arguments to the child functions. Multiple returns don't break the function because again, the child functions can operate even after the parent functions exit.

The process of breaking down functions with multiple arguments into a chain of single return functions is known as currying.

// This function takes a binary function and an argument, and returns a function that can take a second argument

const curry = (binaryFunction, arg) => {
    return function(secondArg){
        return binaryFunction(arg, secondArg);
    };
}

curry(add,2)(7); // is equal to 9

(Update 12/21, I wasn't on Manjaro for longer than a week before an update broke my finicky killer wifi card, and am back on Pop/Debian presently) I've recently switched employers, from a notoriously grindy place to work to a more people-centric place to work. I'm still in training, and consequently don't have any insight yet into how all of that is going to translate into my day to day work, but I've found myself in a place where I didn't foresee myself being- a Manjaro user.

I've dabbled in MANY Linux distributions over the years, and typically use either Ubuntu based distributions (for compatibility/easy targeting) or OpenSUSE Tumbleweed when I want cutting edge package versions. My new employer sent me a Dell XPS 15 with an i9 processor- which also features Qualcomm WiFi that doesn't currently have spectacular support (this support is provided by the ath11k kernel module). None of the Ubuntu, Fedora, OpenSUSE, or Arch ISOs were able to detect the WiFi card out of the box(despite the presence of the ath11k module and brand new kernel version in some cases)– which was a significant problem for me as I had no desire to compile a kernel just to use my wifi. I also didn't particularly want to ask my employer for a different computer, or buy a separate card out of pocket that is better supported as I often saw mentioned as a 'solution'.

I finally tried a Manjaro ISO out of desperation, and was pleased to find that it worked nominally with the WiFi card- until I installed it. I then was able to get things running by taking the steps in an Arch wiki article related to a similarly afflicted XPS model.

I'm definitely still getting the hang of pacman, but I'm already enjoying the presence of the AUR. It's a very nice looking system too, and clearly lots of effort has been put into customizing the look and feel of their GNOME/KDE spins, but I don't think I like the overtness of the Manjaro branding being present in my terminal (as default part of the preconfigured powerline prompt). Additionally, the experience has been rough around the edges overall (weird but non breaking errors in the package manager, lots of trouble with suspend/reboot/sound), though I attribute most of this to the markedly Linux hostile hardware.

I'll aim to update this again in the near future- though I don't see myself switching from my traditionally utilized distros yet, I'm definitely keeping an open mind. Here's hoping things stay stable as I'm ramping up at work, and that better hardware drivers make their way into the kernel.

Enter your email to subscribe to updates.