[{"content":" Preface # Every organization I\u0026rsquo;ve worked for since 2017 has used Kubernetes. Granted, back in 2017, it was still a very new technology that didn’t benefit from the open-source ecosystem that it now does, and things were more challenging to do with it in general.\nMy professional experience with Kubernetes began with reading documentation and trying things locally or in AWS EKS. Local environments like Kind and k3s were new but suffered from severe resource limitations. Despite the improvement in laptop specs over the years, trying to do meaningful local development work, testing, or learning remains a significant challenge due to the complexity of said environments and the cost of managed options like EKS, GKE, and AKS.\nThese challenges, coupled with the scope of my day-to-day work being more Kubernetes centric made my desire for a decent local Kubernetes cluster that wouldn’t cause me to declare bankruptcy or melt my laptop.\nThe Raspberry Pi # I’ve been using Raspberry Pis for several years for various hobbies and personal projects, so it made sense to use one of these, especially with the release of microk8s.\nMicrok8s allowed me to quickly run a cluster and toggle several services off and on as needed. However, the resource limitations of the pi3 and, eventually, pi4 were a significant hurdle. Due to resource strain, building out a cluster with meaningful things like Prometheus, Thanos, Grafana, and Istio was a struggle. Additionally, many core services, like Prometheus, didn’t have arm-based containers available at the time, further limiting my options.\nEnter the ODROID # I needed something with an x86 architecture and a non-trivial amount of RAM, and a friend mentioned the ODROID H2+. It’s a nifty little SBC with a tiny physical footprint, an Intel chip with four cores, and up to 32GB RAM.\nThis was a definite step up from the Pi. I could use microk8s without kubectl commands taking forever to execute due to resources maxing out. I could also deploy those services that didn’t have an arm-based container available.\nI had enough RAM, but it wasn’t long before I started taxing the four core Intel processor. This was when I was learning about Prometheus in-depth, recording rules and capturing metrics of “busy” components like Istio. Running certain queries was hitting the upper limits of the processor in this context.\nThe search continued for a solution where I had a cluster of machines like this ODROID.\nA Note About ODROID # If you’re in the market for something more powerful than a Pi with many great features, I can’t recommend ODROID boards enough. My H2+, purchased in late 2020, is still running strong, and I recently used it to go through the Linux From Scratch project and plan to use it for their BLFS and ALFS projects.\nI should also make it clear that I’m not in any way incentivized to say the above. I’m just a fan of their products.\nThe Turing Pi # Around mid to late 2020, I started seeing blogs and articles about people clustering Raspberry Pis together for various reasons, including as Kubernetes clusters. I happened across this blog post by Jeff Geerling about the Turing Pi and thought it could be the solution I was looking for.\nHowever, by the time I discovered they were a thing, it was next to impossible to get one, much less populate it with Raspberry Pi Compute Modules. Now, in mid-2024, the availability of compute modules and other SBC options is once again reaching pre-pandemic levels.\nBecause of these shortages, there was nothing to do but wait. So that’s what I did, and in early 2022, the Turing Pi 2 was announced, and with it, a Kickstarter campaign. I was lucky enough to quickly get one of the early bird pricing pledges. It took almost a year from my pledge until it landed on my doorstep, and in that time, I still hadn’t been able to source Raspberry Pi Compute Modules. The Turing RK1 modules hadn’t been released yet, and no release date was set because of the ongoing chip shortages.\nThe board sat in its box, unused for about six months, while I searched for CM4 modules that weren’t being scalped. I used that time to pick up some needed hardware to go along with it: a power supply, a mini-ITX case, and an SSD I could use for cluster storage because NVMe drives are still insanely expensive.\nEventually, with no sign that the chip shortage was going to end anytime soon, I cracked when I was able to find four CM4 modules on eBay. Yes, I overpaid, but I finally had something as close as possible to an actual Kubernetes cluster with 16 cores and 32GB of memory. I was able to stand up all of the OSS services I used in my org and accelerate my learning in an environment where if I made a critical mistake, I wasn’t taking down prod or wasting company dollars while I spent time trying to fix it.\nIn Q2 of 2023, it was announced that the Turing RK1 boards were available for preorder, and I jumped at the opportunity because a home k8s lab with 32 cores and 128GB memory? Yes, please. It wasn\u0026rsquo;t cheap of course but I\u0026rsquo;d had the foresight to set money aside in anticipation of their release.\nBy the time they arrived, nearly a year after ordering them, the firmware for the Turing Pi had been updated to make the process of updating the firmware easy to do through the UI but it did require a complete reinstall.\n","date":"June 26, 2024","externalUrl":null,"permalink":"/post/home-k8s-env/","section":"Posts","summary":"Preface # Every organization I’ve worked for since 2017 has used Kubernetes. Granted, back in 2017, it was still a very new technology that didn’t benefit from the open-source ecosystem that it now does, and things were more challenging to do with it in general.\nMy professional experience with Kubernetes began with reading documentation and trying things locally or in AWS EKS. Local environments like Kind and k3s were new but suffered from severe resource limitations. Despite the improvement in laptop specs over the years, trying to do meaningful local development work, testing, or learning remains a significant challenge due to the complexity of said environments and the cost of managed options like EKS, GKE, and AKS.\n","title":"Building a Kubernetes Home Lab","type":"post"},{"content":"","date":"June 26, 2024","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"June 26, 2024","externalUrl":null,"permalink":"/tags/home-lab/","section":"Tags","summary":"","title":"Home Lab","type":"tags"},{"content":"","date":"June 26, 2024","externalUrl":null,"permalink":"/tags/kubernetes/","section":"Tags","summary":"","title":"Kubernetes","type":"tags"},{"content":"","date":"June 26, 2024","externalUrl":null,"permalink":"/categories/personal/","section":"Categories","summary":"","title":"Personal","type":"categories"},{"content":"","date":"June 26, 2024","externalUrl":null,"permalink":"/post/","section":"Posts","summary":"","title":"Posts","type":"post"},{"content":"","date":"June 26, 2024","externalUrl":null,"permalink":"/","section":"Robert Mendal","summary":"","title":"Robert Mendal","type":"page"},{"content":"","date":"June 26, 2024","externalUrl":null,"permalink":"/tags/small-board-computing/","section":"Tags","summary":"","title":"Small Board Computing","type":"tags"},{"content":"","date":"June 26, 2024","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"February 3, 2023","externalUrl":null,"permalink":"/tags/api/","section":"Tags","summary":"","title":"Api","type":"tags"},{"content":"","date":"February 3, 2023","externalUrl":null,"permalink":"/tags/aws/","section":"Tags","summary":"","title":"Aws","type":"tags"},{"content":"","date":"February 3, 2023","externalUrl":null,"permalink":"/tags/ci/cd/","section":"Tags","summary":"","title":"Ci/Cd","type":"tags"},{"content":"","date":"February 3, 2023","externalUrl":null,"permalink":"/tags/coding/","section":"Tags","summary":"","title":"Coding","type":"tags"},{"content":"In part 3 of this series I went over the architecture of the api and how it all works. I\u0026rsquo;m now going to set this up an AWS lightsail instance to run this as well as adding it to systemd so that it starts automatically if the system reboots and restarts in the event of a failure, add in some ssl love with LetsEncrypt and, set up log rotation because I did enable logging with echo in case we encounter errors I can troubleshoot.\nPreface # I ended up purchasing a domain for this project and have Cloudflare in front of it as a proxy for some analytics and a bit of security. The domain was ~$24 for 2 years and I\u0026rsquo;m only utilizing Cloudflares free tier for this. I\u0026rsquo;ll tally up the total cost for this project at the end.\nThe Infrastructure # As I mentioned in an earlier post I\u0026rsquo;ll be running this on an AWS Lightsail instance because it\u0026rsquo;s the easiest choice and setting one up is easier than setting up an ec2 instance. There\u0026rsquo;s just a couple of things that need to be done once it\u0026rsquo;s provisioned.\nGive it a static ip. This is free in AWS as long as the ip is attached to a lightsail instance. Open a few ports on the firewall. In this case 80 and 443 for http/s traffic. 80 won\u0026rsquo;t actually be used though as we\u0026rsquo;ll redirect via Cloudflare. Other than that the server is ready to dive into and get configured.\nThe Hiccup # So now we just run go build woin-api and get the binary onto our vps right? Well yes but actually no. Architecture matters, specifically system architecture. I wrote the code, and this blog, on a macbook with an arm processor so running go build results in a binary that works only on arm64 and of course that isn\u0026rsquo;t what our lightsail instance is. Running uname -m on the server spits out x86_64 so how do we fix this?\nWell we could just clone the repo locally onto the server and build it there. That\u0026rsquo;s pretty easy to do. But what if this API goes viral (0% chance) and is used by a ton of people? I\u0026rsquo;d need to provision a larger server and maybe it won\u0026rsquo;t be x86_64 so should I settle for always having to clone a repo and build locally? That seems annoying.\nSo how can we cover all architectures especially in an automated fashion? Well my personal code is in Github and we can use actions, their CI/CD tooling, to automatically build the binary when we push a new tag up to the main branch. This is achieved easily enough with GoReleaser\nThe result is great. All we ever need to do now is just wget the binary from the releases page. SSL Time # I want my site to have SSL and you should too. The easiest answer here is getting a LetsEncrypt cert using CertBot. Following the instructions is easy enough. The auto-renewal requires port 80 to be open on your host though. If you\u0026rsquo;re not comfortable leaving it open then it\u0026rsquo;s on you to remeber to renew the cert every 90 days and will still require you to open port 80 to do so even if the process takes all of 30 seconds.\nAs a part of the cert request you\u0026rsquo;ll need to prove you own your domain and the easiest thing to do is create a TXT record in the same place your A record is, in this case it\u0026rsquo;s Cloudflare for me.\nHow Do We Tell Echo to Use the Cert? # Echo docs to the rescue. I did try their AutoTLS Recipe but couldn\u0026rsquo;t get it to work correctly so opted for the first link.\nLogging and Log Management # Now\u0026rsquo;s a good time to discuss how we\u0026rsquo;ll handle logs and recovery in the event of a fatal error.\nEcho has built in logging features of course and they\u0026rsquo;re pretty configurable. I utilize this so that I get information that I care about seeing. Note that everything in this section lives in my main function in the api main.go.\n// Middleware e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: \u0026#34;time=${time_rfc3339_nano}, remote_ip=${remote_ip}, host=${host}, method=${method}, status=${status}, uri=${uri}, user_agent=${user_agent}, error=${error}, latency=${latency_human}\\n\u0026#34;, Output: f, })) e.Use(middleware.Recover()) The formatting is pretty self-explanatory in what I\u0026rsquo;m actually logging. The e.Use(middleware.Recover()) is another part of the echo middleware and I use it here simply to recover from panics should they happen though it is customizable.\nWhat may seem out of place if the f variable in the Output part of the logging declaration.\nAbove the middleware logging declaration I set up a writer to write to a log file as seen here.\n// Writer for Echo Logging f, err := os.OpenFile(\u0026#34;/var/log/woin/server.log\u0026#34;, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(fmt.Sprintf(\u0026#34;Error opening file: %v\u0026#34;, err)) } defer f.Close() Here I\u0026rsquo;m just using os.OpenFile, the f mentioned above, from the standard library and handling any error that may come up when opening the file though it shouldn\u0026rsquo;t happen because I\u0026rsquo;m telling os.OpenFile that it can r/w, create and append to the file so it \u0026ldquo;should\u0026rdquo; no matter what but it\u0026rsquo;s always best to handle any potential issues anyway.\nManaging Your Logs # Log files will grow over time and while it\u0026rsquo;s unlikely to chew up all our disk space in this instance it\u0026rsquo;s always a good idea to plan for the possability of this happening. Enter logrotate.\nI\u0026rsquo;m using a simple config here to make sure my log files don\u0026rsquo;t run amok. The below config file is pretty self explanatory but all options listed are explained in detail in the logrotate link above.\n/var/log/woin/*.log { weekly missingok rotate 12 compress notifempty create 0666 root root sharedscripts postrotate systemctl reload woin-api endscript } Systemd # If you\u0026rsquo;re not aware of what systemd is you can find out more here. I\u0026rsquo;ll be using it to add the api binary as a system service on my lightsail instance so that it automatically starts at boot and attempts to restart if it crashes.\nFirst I\u0026rsquo;ll get my service file in place, touch /etc/systemd/system/woin-api.service with the below contents\u0026hellip;\n[Unit] Description=WOIN API After=network.target StartLimitIntervalSec=0 [Service] Type=simple Restart=always RestartSec=10 User=root ExecStart=/home/woin-api WorkingDirectory=/home [Install] WantedBy=multi-user.target With the configuration file in place we run the following to get it enabled in systemd\u0026hellip;\nsudo systemctl daemon-reload \u0026amp;\u0026amp; \\ sudo systemctl enable woin-api \u0026amp;\u0026amp; \\ sudo service woin-api start And that\u0026rsquo;s it. Now our api is running and will continue to even if the system reboots. Further, you can see it\u0026rsquo;s status by running sudo service woin-api status. You should see something like this\u0026hellip; Conclusion # That\u0026rsquo;s it. Our api is up and running. If we curl our server endpoint we receive the expected response\u0026hellip;\ncurl https://foo.bar/v1/species/Borian {\u0026#34;status\u0026#34;:200,\u0026#34;message\u0026#34;:\u0026#34;success\u0026#34;,\u0026#34;data\u0026#34;:{\u0026#34;species\u0026#34;:{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda793\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Borian\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;carousing, hardy, [crafting], engineering, appraisal\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Darksight, Iron Constitution, Tinkerer, Long-lived, Personable, Small\u0026#34;,\u0026#34;age_multiplier\u0026#34;:3,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;small\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NEW\u0026#34;}}} And curling the whole collection of species works too\u0026hellip;\ncurl https://foo.bar/v1/species {\u0026#34;status\u0026#34;:200,\u0026#34;message\u0026#34;:\u0026#34;success\u0026#34;,\u0026#34;data\u0026#34;:{\u0026#34;data\u0026#34;:[{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda75e\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Tauran\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:1,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;astrogation, piloting, brawling, axes, tracking, scent\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Horns, Charge, Direction Sense, Stubborn, Stoic\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda781\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Fornian\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:1,\u0026#34;wil\u0026#34;:2,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[scientific]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Plant, Spores, Scent Messages, Immune To Poison, Ambulation, Darksight, Underground Dwellers\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda761\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Deva\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:2,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;history, local knowledge, [artistic], insight, religion\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Varied, Ageless, Reincarnated, Deathless Calm, Memory Of Past Lifetimes\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;ZPG\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda76d\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Houseki\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:1,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:1,\u0026#34;skill_choices\u0026#34;:\u0026#34;[technical], brawling, zero-g, mining, hardy\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Crystalline, Always Growing, Crystal Healing, Jagged Crystal, Slow \\u0026 Careful, Implacable, Long-lived\u0026#34;,\u0026#34;age_multiplier\u0026#34;:3,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda776\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Spartan\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[combat], intimidate, carousing\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Berserker, Redundant Organs, Warlike\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NEW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda77b\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Sylvan Elf\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:2,\u0026#34;skill_choices\u0026#34;:\u0026#34;animal handling, bows, climbing, herbalism, nature, running, stealth, survival, tracking\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Fey, Healthy, Nature Affinity, Trance, Tree-Dwellers, Unimpeded\u0026#34;,\u0026#34;age_multiplier\u0026#34;:5,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;OLD\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda797\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Werewolf\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;scent, tracking, running, hardy\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Transformation, Involuntary Transformation, Darksight, Silver Vulnerability\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;DD\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda799\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Clockman\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:2,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:1,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;engineering, astronomy, law, history, medicine, astrogation\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Mechanoid, Repair, Wind-up, Internal Clock, Attachments, Durable\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda75f\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Garga\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[crafting], [artistic], mathematics\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Amphibian, Great Leap, Webbed Limbs, Long Tongue, Chameleon\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda77c\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Jovian\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;hardy, carrying, mining, high-g, computers, [technical]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Sturdy Frame, Crushing Environment, Set In Their Ways, Hard To Move, Inexhaustible\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;XEN\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda753\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Human\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:2,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[any]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Varied, Learners, Enduring\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;ALL\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda796\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Gorilla\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;brawling, carrying, interrogation, intimidation\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Mighty Load, Thick Fur, Roar, I Am Kang!\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;large\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;JDW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda77f\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Warped\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:4,\u0026#34;skill_choices\u0026#34;:\u0026#34;perception, insight, foresight, astrogation, [psionic]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Crazed Minds, Warped Psionics, See Ghosts\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda78f\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Bragi\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:1,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;piloting, astrogation, perception, swimming, art\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Aquatic, Astrogator Adaptive Gills, Suckers, Perceptive, Long-lived\u0026#34;,\u0026#34;age_multiplier\u0026#34;:2,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda795\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Grey\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:1,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:3,\u0026#34;skill_choices\u0026#34;:\u0026#34;telepathy, negotiation, linguistics, insight, astrogation\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Empathy, Telepathic Message, Telekinesis, Long-lived, Telekinetic Shield\u0026#34;,\u0026#34;age_multiplier\u0026#34;:2,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;small\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda752\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Saurian\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:3,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;survival, climbing, running, spears\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Reptilian, Always On The Move, Balancing Tail, Inexhaustible, Poison Breath, Nomad\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda77e\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Drahzik\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:1,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;hunting, tracking, stealth, rifles, spears, climbing\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Reptilian, Scent, Fast Healing, Cold-blooded, Infravision, Traditional Gear\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda764\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Belter\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:1,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;acrobatics, low-g, zero-g, jumping, astronomy, carousing, appraisal, mining, engineering, zero-g sports, gambling\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Frail Form, Flexible, Hard-To-Hit, Hold Breath\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;XEN\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda76b\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Ogron\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;carrying, hardy, bravery, intimidate\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Dull-witted, Smelly, Brawny, Large, Strong, Stronger With Age\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;large\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NEW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda78b\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Augmented\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;hardy, perception, reactions, [crafting], [trivia], [gaming], [scientific], [technical]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Alteration, Adaptive, Inert\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NOW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda78e\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Grand Elf\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:2,\u0026#34;psi_mag_chi\u0026#34;:3,\u0026#34;skill_choices\u0026#34;:\u0026#34;muskets, pistols, swords, alchemy, law, intimidate, leadership, sailing, [musical]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Fey, Magic Sense, Meditation, Naturally Magical, Cultural Weapon, Long-lived, Magical\u0026#34;,\u0026#34;age_multiplier\u0026#34;:5,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;OLD\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda793\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Borian\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;carousing, hardy, [crafting], engineering, appraisal\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Darksight, Iron Constitution, Tinkerer, Long-lived, Personable, Small\u0026#34;,\u0026#34;age_multiplier\u0026#34;:3,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;small\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NEW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda757\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Trantor\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;hardy, carrying, concentration, history, insight\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Tusks, Trunk, Never Forget, Slow To Move, Lasting Minds\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;3d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;large\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda765\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Smallfolk\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:2,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:2,\u0026#34;psi_mag_chi\u0026#34;:1,\u0026#34;skill_choices\u0026#34;:\u0026#34;[crafting], bluffing, brewing, cooking, diplomacy, farming, fishing, slings, stealth, thievery\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Stubborn, Evasion\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;small\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;OLD\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda783\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Argon\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;nature, diplomacy, perception, reactions, carrying, intimidation\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Mighty Load, Commune With Nature, Pacifist, Thick Fur, Roar\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;large\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda794\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Clone\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:1,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[developmental], [physical]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Ingrained Skill Package, Fast Healing, Slow Aging\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;XEN\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda79a\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Pagozan\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;climbing, computers, piloting, stealth\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Reptilian, Camouflage, Motion-sight, Tail, Regenerative\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda759\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Kanid\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;perception, scent, insight, tracking, running, [social]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Pack-bonding, Fur Coat, Chasers, Scent, Alcohol Weakness, Social\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda75b\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Changeling\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:3,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:2,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;bluffing, insight, stealth\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Changeling, Regeneration, Morphed Weapon, Acid Vulnerability, Ageless\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda771\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Raptor\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:3,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:3,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;tracking, engineering, intimidation, running, perception\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Reptilian, Fierce Jaws, Pounce\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;3d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda775\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Half-ghost\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:2,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:3,\u0026#34;skill_choices\u0026#34;:\u0026#34;[any]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Semi-corporeal, No Biology, Manifest, Darksight\u0026#34;,\u0026#34;age_multiplier\u0026#34;:5,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;SOM\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda78d\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Dhampir\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;history, stealth, disguise, bluffing\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Vampiric Bite, Spider-climb, Sunlight Sensitivity, Biological Immunity\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;DD\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda791\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Ogre\u0026#34;,\u0026#34;str\u0026#34;:3,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:3,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;brawling, carrying, hardy, bravery, intimidate, [melee weapon]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Thick Hide, Smelly, Darksight, Acid Blood, Strong\u0026#34;,\u0026#34;age_multiplier\u0026#34;:3,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;large\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;OLD\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda792\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Ginean\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:1,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;climbing, computers, piloting, [artistic], [academic]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Reptilian, Camouflage, Motion-sight, Regenerative, Long-lived\u0026#34;,\u0026#34;age_multiplier\u0026#34;:3,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda75d\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Kithik\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:1,\u0026#34;skill_choices\u0026#34;:\u0026#34;survival, astrogation, polearms\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Insectoid, Venomous Bite, Four Arms, Short-lived\u0026#34;,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda756\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Chorax\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:3,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:1,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;biology, xenology, stealth, medicine, hunting\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Insectoid, Eight-legged, Webspinner, Revolting, Poison Immunity, Darksight\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda754\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Roden\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:2,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[subterfuge], appraisal, perception\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Gnaw Away, Tail, Light Sensitivity, Navigators, Scamper\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;small\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda75a\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Adraxi\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;tracking, stealth, hunting, brawling, climbing\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Avian, Reptilian, Serrated Beak, Perch, Leathery Wings\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda767\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Pajak\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[subterfuge], [scientific], bureaucracy, hypnotism\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Insectoid, Compound Eyes, Thin Bones, Scrawny, Bite, Short-lived\u0026#34;,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda786\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Yakan\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;nature, diplomacy, perception, reactions, carrying, intimidation\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Mighty Load, Thick Fur, Roar, Cannot Speak\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;large\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda75c\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Orangutan\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:3,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;acrobatics, carousing, gambling, reactions\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;King Of The Swingers, Great Leap, Fast\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;JDW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda76e\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Torbanite\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:4,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:2,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[medical], [scientific]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Crsystalline, No Pain, Limited Motion, Enduring\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda777\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Mutant\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:1,\u0026#34;skill_choices\u0026#34;:\u0026#34;hardy, intimidate, resistance, survival, [crafting], [trivia], [gaming], stealth, disguise\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Mutation\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NOW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda77d\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Solurial\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[outdoor], [artistic]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Plant, Lashing Vines, Ponderous, Photosynthesis, Extended Families, Camouflage, Roots, Long-lived\u0026#34;,\u0026#34;age_multiplier\u0026#34;:7,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;large\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda763\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Gris\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:2,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:1,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[artistic], [academic], [scientific]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Eat Anything, Poor Sight, Philosophical, Scholastic, Advice\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda769\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Chimp\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;acrobatics, brawling, carousing, movies, gambling\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Natural Climbers, Zero-G, Great Leap, Throwers, Agile, Weak-willed\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;small\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;JDW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda76f\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Zetan\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:1,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[artistic]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Blur, Speed Dodge, Weak-willed, Unfocused, Unstoppable, Short-lived\u0026#34;,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda77a\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Mutant\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:2,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:1,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;bluffing, disguise, running, stealth\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Radiation Resistance, Mutation\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;JDW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda782\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Felan\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:1,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;acrobatics, climbing, jumping, [unarmed fighting], reactions, appraisal, bluffing, stealth, negotiating\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Agile, Fast, Land On Your Feet, Claws\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NEW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda760\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Zouklan\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:3,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[outdoor], spears, rifles, riding\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Climbers, Great Leap\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda774\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Hologram\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:2,\u0026#34;cha\u0026#34;:2,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[scientific], [social]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Mechanoid, Intangible, Free Movement, Immortal, Limited Range, Ageless\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;0\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda785\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Orc\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:3,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[combat], blacksmithing, carousing, hunting, intimidation, mining, running, tracking\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Glory, Darksight, Bloodlust\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;OLD\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda758\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Synthetic\u0026#34;,\u0026#34;str\u0026#34;:3,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;computers, engineering, medicine, piloting, [artistic], [academic], [scientific]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Mindless, Deterministic, Electronic Vulnerability, Automaton, Skill Package, Factory Specs\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;XEN\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda762\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Venetian\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:2,\u0026#34;psi_mag_chi\u0026#34;:3,\u0026#34;skill_choices\u0026#34;:\u0026#34;reactions, acrobatics, perception, concentration, religion, [scientific]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Naturally Psionic, Acute Hearing, Learned, Disciplined, Long-lived, Logical\u0026#34;,\u0026#34;age_multiplier\u0026#34;:5,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NEW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda76a\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Jamila\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:2,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:1,\u0026#34;skill_choices\u0026#34;:\u0026#34;[social], [subterfuge], [artistic]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Pheromones, Draining Life Force, Diplomats\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda772\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Treem\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:2,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[outdoor], [social], [artistic]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Plant, Seeds, Immovable, Cannot Jump\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda778\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Betrux\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;engineering, computers, electronics, climbing, perception\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Insectoid, Thick Shell, Antennae, Blind, Slow, Mandibles, Subspecies\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda784\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Android\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;computers, engineering, running, linguistics, [technical]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Mechanoid, Ageless, Modification\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;any\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NEW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda78c\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Neron\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:2,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:1,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[medical], insight, linguistics, oceanography, polearms\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Aquatic, Water-breathers, Medical Marvel, Marine Friends, Darksight, Water Healing, Water Bolt\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda790\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Vouki\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:2,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[social], hypnotism\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Reptilian, Hypnotism, Climb, Venomous Bite\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda768\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Chosen\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:2,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:3,\u0026#34;skill_choices\u0026#34;:\u0026#34;negotiating, tactics, [crafting]. [trivia], [gaming], [scientific]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Fast-healing, Skill Focus, Destiny\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NOW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda76c\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Charon\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;survival, hunting, engineering, [melee weapon], tracking, running, intimidation\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Irradiated, Grotesque, Mutation\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda770\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Clone\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:1,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;perception, insight, concentration, meditation, bravery, resistance, hardy, climbing, swimming, jumping, carrying, running, acrobatics, zero-g, throwing\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Ingrained Skill Package, Fast Healing\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;JDW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda780\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Robot\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;computers, engineering, running, linguistics, [technical]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Mindless, Deterministic, Electronic Vulnerability, Automaton, Modification Choice\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;any\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;JDW\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda789\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Eladrin\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:1,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:3,\u0026#34;skill_choices\u0026#34;:\u0026#34;[lore], [outdoor], [magical]\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Step Through The Dreaming, Cold Iron\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;ZPG\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda798\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Acorax\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:2,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:1,\u0026#34;skill_choices\u0026#34;:\u0026#34;acrobatics, perception, stealth, knives, bluffing, disguise, chemistry\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Avian, Hollow-boned, Beak, Mimickry\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;small\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda755\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Laerrek\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:3,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:1,\u0026#34;skill_choices\u0026#34;:\u0026#34;perception, insight, foresight, reactions, clairvoyance\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Truesight, Foresight, Insight, Long-lived\u0026#34;,\u0026#34;age_multiplier\u0026#34;:2,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda766\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Gobber\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;thievery, climbing, stealth, survival, knives\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Pack Attack, Darksight, Bright Light, Natural Cunning, Scavengers, Snatch, Between The Legs\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;small\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda773\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Mountain Dwarf\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:2,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[crafting], alchemy, axes, hammers, carousing, hammers, mining\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Darksight, Iron Constitution, Sturdy, Earthy, Long-lived, Stubborn\u0026#34;,\u0026#34;age_multiplier\u0026#34;:3,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;small\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;OLD\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda779\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Hellion\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:2,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;bluffing, engineering, rapiers, pistols\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Natural Weapons, Superior Darksight, Fire Resistance, Antipsyker\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda787\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Molluk\u0026#34;,\u0026#34;str\u0026#34;:2,\u0026#34;agi\u0026#34;:1,\u0026#34;end\u0026#34;:2,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;hardy, carrying, meditation, concentration, history\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Crsytalline, Stone Fist, Bulky, Natural Camouflage, Long-lived\u0026#34;,\u0026#34;age_multiplier\u0026#34;:10,\u0026#34;damage\u0026#34;:\u0026#34;3d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda788\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Simp\u0026#34;,\u0026#34;str\u0026#34;:1,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:1,\u0026#34;cha\u0026#34;:0,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:2,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;jumping, acrobatics, throwing, engineering, gambling\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Natural Climbers, Zero-G, Great Leap, Throwers, Agile, Weak-willed\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;GSC\u0026#34;},{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda78a\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Deepling\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:2,\u0026#34;end\u0026#34;:0,\u0026#34;int\u0026#34;:0,\u0026#34;log\u0026#34;:2,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:2,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;[crafting], bluffing, engineering, sailing, rapiers, pistols, rifles\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Natural Weapons, Superior Darksight, Fire Resistance\u0026#34;,\u0026#34;age_multiplier\u0026#34;:1,\u0026#34;damage\u0026#34;:\u0026#34;2d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;medium\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;ZPG\u0026#34;}]}} ","date":"February 3, 2023","externalUrl":null,"permalink":"/post/woin-api-pt-4/","section":"Posts","summary":"In part 3 of this series I went over the architecture of the api and how it all works. I’m now going to set this up an AWS lightsail instance to run this as well as adding it to systemd so that it starts automatically if the system reboots and restarts in the event of a failure, add in some ssl love with LetsEncrypt and, set up log rotation because I did enable logging with echo in case we encounter errors I can troubleshoot.\n","title":"Finishing the WOIN API","type":"post"},{"content":"","date":"February 3, 2023","externalUrl":null,"permalink":"/tags/github-actions/","section":"Tags","summary":"","title":"Github Actions","type":"tags"},{"content":"","date":"February 3, 2023","externalUrl":null,"permalink":"/tags/linux/","section":"Tags","summary":"","title":"Linux","type":"tags"},{"content":"","date":"February 3, 2023","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","date":"February 3, 2023","externalUrl":null,"permalink":"/tags/ttrpg/","section":"Tags","summary":"","title":"Ttrpg","type":"tags"},{"content":"","date":"February 3, 2023","externalUrl":null,"permalink":"/tags/woin/","section":"Tags","summary":"","title":"Woin","type":"tags"},{"content":"","date":"February 3, 2023","externalUrl":null,"permalink":"/series/woin-api/","section":"Series","summary":"","title":"WOIN API","type":"series"},{"content":"","date":"January 28, 2023","externalUrl":null,"permalink":"/tags/go/","section":"Tags","summary":"","title":"Go","type":"tags"},{"content":"In part 2 of this series I laid out the foundation for this project as well as the issues and solutions I decided on. So now lets actually write some code.\ngo mod init foo # I first made a Github repo, cloned it locally and ran go mod init to start. I did some searching on directory structure and organization for an API with the intent to not end up with a monolith of a main.go file. Since I\u0026rsquo;ll be breaking this API into modules I settled on the following\u0026hellip;\nconfigs - Where I\u0026rsquo;ll create separate files to load the Mongo creds using godotenv and then create the actual connection to the db. controllers - The file here is where I\u0026rsquo;ll load collections and perform CRUD operations on them. models - Here is where I\u0026rsquo;ll specify the format of the data I expect to pull from the DB and also send to the DB. public - I can create HTML files here for echo to display. responses - The file here determines how responses are returned. They\u0026rsquo;ll include the HTTP code (e.g. 200), the message (e.g. Success) and a JSON response of the data if any. routes - This is where I\u0026rsquo;ll build my endpoints such as GET, PUT, etc. Starting with this layout will make this API easier to maintain over time.\nHello Mongo # Since this API will query data from my Mongo DB we should probably start with connecting to it. We\u0026rsquo;ll establish both the loading/verification of the .env file and the DB connection in the configs dir and in different files. Lets start with loading the .env file.\nTo connect to Mongo we need to set a Mongo URI environment variable which is easy enough. We get our specific string from our Mongo instance, modify it if we\u0026rsquo;re using a user/pass or x509 cert and simply store the string in a file named .env.\nWith our Mongo connection string stored locally and not in code we need to confirm our .env file exists, that it contains data and then return that data to whatever calls it to establish a connection. We do so with the below code.\npackage configs import ( \u0026#34;github.com/joho/godotenv\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func MongoURI() string { if err := godotenv.Load(); err != nil { log.Println(\u0026#34;No .env file found\u0026#34;) } uri := os.Getenv(\u0026#34;MONGOURI\u0026#34;) if uri == \u0026#34;\u0026#34; { log.Fatal(\u0026#34;You must set your \u0026#39;MONGOURI\u0026#39; environment variable. See\\n\\t https://www.mongodb.com/docs/drivers/go/current/usage-examples/#environment-variable\u0026#34;) } return uri } This is fairly easy to grok; godotenv.Load() attempts to load a .env file and it if can\u0026rsquo;t find one it throws an error. If it finds the file then it sets the string inside it as an environment variable.\nThe uri variable reads in that environment variable. We then check the variable to make sure it\u0026rsquo;s not an empty string, if it is then we throw an error. If it\u0026rsquo;s not then we return that data to what called it.\nMaking the Connection # Now that we\u0026rsquo;re reading in our Mongo creds we need to establish a connection to it and we should also ping the DB to make sure we\u0026rsquo;re actually connected. We can achieve this with the below.\npackage configs import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;go.mongodb.org/mongo-driver/mongo\u0026#34; \u0026#34;go.mongodb.org/mongo-driver/mongo/options\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; ) func ConnectDB() *mongo.Client { client, err := mongo.NewClient(options.Client().ApplyURI(MongoURI())) if err != nil { log.Fatal(err) } ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) err = client.Connect(ctx) if err != nil { log.Fatal(err) } //ping the database err = client.Ping(ctx, nil) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Connected to MongoDB\u0026#34;) return client } Let\u0026rsquo;s break it down a bit. First we create a client variable which creates a new mongo client which calls the MongoURI function we created earlier then it checks it for an error and stops if it finds one.\nThe ctx var creates a 10 second timeout for this connection and if it exceeds the 10 seconds it throws a timeout error.\nLastly, we ping the database and assuming we didn\u0026rsquo;t hit an error along the way we\u0026rsquo;re greeted with Connected to MongoDB.\nYeah I\u0026rsquo;ve Done Some Modeling # Before we go further we need to specify how data should be modeled. Specifically, I mean we should decide how we want our data to look when it\u0026rsquo;s called from the database and, more importantly, what format it should conform to when puts or updates are made via the API.\nWhen we imported our data into MongoDB it automatically assigns it an ID in BSON (binary JSON) and in order to read or write this type we need to use a specific go import go.mongodb.org/mongo-driver/bson/primitive. This will be our only import in this package which is aptly named models and will, unsurprisingly, go into our models dir.\nThis code will be a series of struct types that mirror the row names in our database for each corresponding data type. Let\u0026rsquo;s look at some code\u0026hellip;\npackage models import \u0026#34;go.mongodb.org/mongo-driver/bson/primitive\u0026#34; type Species struct { Id primitive.ObjectID `bson:\u0026#34;_id\u0026#34; json:\u0026#34;_id,omitempty\u0026#34;` Name string `json:\u0026#34;name\u0026#34; validate:\u0026#34;required\u0026#34;` STR int `json:\u0026#34;str\u0026#34; validate:\u0026#34;required\u0026#34;` AGI int `json:\u0026#34;agi\u0026#34; validate:\u0026#34;required\u0026#34;` END int `json:\u0026#34;end\u0026#34; validate:\u0026#34;required\u0026#34;` INT int `json:\u0026#34;int\u0026#34; validate:\u0026#34;required\u0026#34;` LOG int `json:\u0026#34;log\u0026#34; validate:\u0026#34;required\u0026#34;` WIL int `json:\u0026#34;wil\u0026#34; validate:\u0026#34;required\u0026#34;` CHA int `json:\u0026#34;cha\u0026#34; validate:\u0026#34;required\u0026#34;` REP int `json:\u0026#34;rep\u0026#34; validate:\u0026#34;required\u0026#34;` LUC int `json:\u0026#34;luc\u0026#34; validate:\u0026#34;required\u0026#34;` PSI_MAG_CHI int `json:\u0026#34;psi_mag_chi\u0026#34; validate:\u0026#34;required\u0026#34;` Skill_Choices string `json:\u0026#34;skill_choices,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Exploits string `json:\u0026#34;exploits,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Age_Multiplier int `json:\u0026#34;age_multiplier,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Damage string `json:\u0026#34;damage,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Size string `json:\u0026#34;size,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Source string `json:\u0026#34;source,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` } type Origin struct { Id primitive.ObjectID `bson:\u0026#34;_id\u0026#34; json:\u0026#34;_id,omitempty\u0026#34;` Name string `json:\u0026#34;name\u0026#34; validate:\u0026#34;required\u0026#34;` Prerequisite string `json:\u0026#34;prerequisite,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` STR int `json:\u0026#34;str\u0026#34; validate:\u0026#34;required\u0026#34;` AGI int `json:\u0026#34;agi\u0026#34; validate:\u0026#34;required\u0026#34;` END int `json:\u0026#34;end\u0026#34; validate:\u0026#34;required\u0026#34;` INT int `json:\u0026#34;int\u0026#34; validate:\u0026#34;required\u0026#34;` LOG int `json:\u0026#34;log\u0026#34; validate:\u0026#34;required\u0026#34;` WIL int `json:\u0026#34;wil\u0026#34; validate:\u0026#34;required\u0026#34;` CHA int `json:\u0026#34;cha\u0026#34; validate:\u0026#34;required\u0026#34;` REP int `json:\u0026#34;rep\u0026#34; validate:\u0026#34;required\u0026#34;` LUC int `json:\u0026#34;luc\u0026#34; validate:\u0026#34;required\u0026#34;` PSI_MAG_CHI int `json:\u0026#34;psi_mag_chi\u0026#34; validate:\u0026#34;required\u0026#34;` Skill_Choices string `json:\u0026#34;skill_choices,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Exploits string `json:\u0026#34;exploits,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Years string `json:\u0026#34;size,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Source string `json:\u0026#34;source,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` } type Career struct { Id primitive.ObjectID `bson:\u0026#34;_id\u0026#34; json:\u0026#34;_id,omitempty\u0026#34;` Name string `json:\u0026#34;name\u0026#34; validate:\u0026#34;required\u0026#34;` STR int `json:\u0026#34;str\u0026#34; validate:\u0026#34;required\u0026#34;` AGI int `json:\u0026#34;agi\u0026#34; validate:\u0026#34;required\u0026#34;` END int `json:\u0026#34;end\u0026#34; validate:\u0026#34;required\u0026#34;` INT int `json:\u0026#34;int\u0026#34; validate:\u0026#34;required\u0026#34;` LOG int `json:\u0026#34;log\u0026#34; validate:\u0026#34;required\u0026#34;` WIL int `json:\u0026#34;wil\u0026#34; validate:\u0026#34;required\u0026#34;` CHA int `json:\u0026#34;cha\u0026#34; validate:\u0026#34;required\u0026#34;` REP int `json:\u0026#34;rep\u0026#34; validate:\u0026#34;required\u0026#34;` LUC int `json:\u0026#34;luc\u0026#34; validate:\u0026#34;required\u0026#34;` PSI_MAG_CHI int `json:\u0026#34;psi_mag_chi\u0026#34; validate:\u0026#34;required\u0026#34;` Prerequisites string `json:\u0026#34;prerequisites,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Skill_Choices string `json:\u0026#34;skill_choices,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Exploits string `json:\u0026#34;exploits,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Years string `json:\u0026#34;size,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Source string `json:\u0026#34;source,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` } type UniversalExploit struct { Id primitive.ObjectID `bson:\u0026#34;_id\u0026#34; json:\u0026#34;_id,omitempty\u0026#34;` Name string `json:\u0026#34;name\u0026#34; validate:\u0026#34;required\u0026#34;` Type string `json:\u0026#34;type,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Description string `json:\u0026#34;description,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` } type CrExploit struct { Id primitive.ObjectID `bson:\u0026#34;_id\u0026#34; json:\u0026#34;_id,omitempty\u0026#34;` Name string `json:\u0026#34;name\u0026#34; validate:\u0026#34;required\u0026#34;` Source string `json:\u0026#34;source,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Description string `json:\u0026#34;description,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` Prerequisites string `json:\u0026#34;prerequisites,omitempty\u0026#34; validate:\u0026#34;required\u0026#34;` } So I\u0026rsquo;ve created a struct with how I want each call (GET, PUT, UPDATE) to look for each corresponding collection in the DB. The columns are pretty repetitive for each struct in this case so I won\u0026rsquo;t explain each row. This is a happy accident in this project as depending on what your API is for there may be many many more.\nReading from left to right we first start with Id which is a primitive ObjectID type and we specify two acceptable data types, either bson or json and in the case of JSON we\u0026rsquo;ve added omitempty. The reason I added this is perhaps I want to PUT data into the Mongo DB using the API. Since Mongo automatically assigns the object ID we can safely omit this information in the event we\u0026rsquo;re doing a PUT. Other data types we\u0026rsquo;re using here are string and int but others exist of course.\nPublic # This is the directory where I\u0026rsquo;ll setup my stretch goal hopefully. It\u0026rsquo;s the directory where I\u0026rsquo;ll put publicaly accessible site files. That HTML class I took in college is really paying off and I created a basic index.html file here which just points towards the main WOIN site.\n\u0026lt;html\u0026gt; \u0026lt;p\u0026gt;Check out \u0026lt;a href=\u0026#34;http://www.woinrpg.com\u0026#34;\u0026gt;WOIN\u0026lt;/a\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/html\u0026gt; The stretch goal is to add a webapp for character creation and documentation for the API.\nResponses # This file is pretty simple. All I\u0026rsquo;m doing here is declaring how responses to API calls are structured. I\u0026rsquo;m using a struct to ensure that every response returns an http status like 200, 404, 503, a message like Success or Failure and the data returned from the call to the DB.\npackage responses import ( \u0026#34;github.com/labstack/echo/v4\u0026#34; ) type Response struct { Status int `json:\u0026#34;status\u0026#34;` Message string `json:\u0026#34;message\u0026#34;` Data *echo.Map `json:\u0026#34;data\u0026#34;` } Here is an example of a GET response\u0026hellip;\n{\u0026#34;status\u0026#34;:200,\u0026#34;message\u0026#34;:\u0026#34;success\u0026#34;,\u0026#34;data\u0026#34;:{\u0026#34;species\u0026#34;:{\u0026#34;_id\u0026#34;:\u0026#34;6305358afc82b7f384fda793\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;Borian\u0026#34;,\u0026#34;str\u0026#34;:0,\u0026#34;agi\u0026#34;:0,\u0026#34;end\u0026#34;:1,\u0026#34;int\u0026#34;:1,\u0026#34;log\u0026#34;:0,\u0026#34;wil\u0026#34;:0,\u0026#34;cha\u0026#34;:1,\u0026#34;rep\u0026#34;:0,\u0026#34;luc\u0026#34;:0,\u0026#34;psi_mag_chi\u0026#34;:0,\u0026#34;skill_choices\u0026#34;:\u0026#34;carousing, hardy, [crafting], engineering, appraisal\u0026#34;,\u0026#34;exploits\u0026#34;:\u0026#34;Darksight, Iron Constitution, Tinkerer, Long-lived, Personable, Small\u0026#34;,\u0026#34;age_multiplier\u0026#34;:3,\u0026#34;damage\u0026#34;:\u0026#34;1d6\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;small\u0026#34;,\u0026#34;source\u0026#34;:\u0026#34;NEW\u0026#34;}}} Routes # Here I\u0026rsquo;m defining where API calls go. I\u0026rsquo;m only implementing GET routes at this time because I\u0026rsquo;m still learning how to build an API correctly and don\u0026rsquo;t want to risk opening myself up to a bad actor by having PUT, UPDATE or DELETE abilities for now.\nHere\u0026rsquo;s an example of the base (root) route and what it looks like to GET a species or all species in WOIN.\npackage routes import ( \u0026#34;github.com/labstack/echo/v4\u0026#34; \u0026#34;woin-api/controllers\u0026#34; ) func BaseRoute(e *echo.Echo) { e.GET(\u0026#34;/\u0026#34;, controllers.GetBase) } func SpeciesRoute(e *echo.Echo) { e.GET(\u0026#34;/v1/species/:speciesName\u0026#34;, controllers.GetSpecies) e.GET(\u0026#34;/v1/species\u0026#34;, controllers.GetAllSpecies) } Notice the routes contain paths that will exist in my url as well as a version, in this case v1. I\u0026rsquo;ve included a version because over time I expect to improve on this and may overhaul it entirely once someone reads this blog and informs me it\u0026rsquo;s a steaming pile of trash.\nYou\u0026rsquo;ll also notice in the imports section that I\u0026rsquo;m using a controllers module to help with the routes. This file imports our other modules as well as loading in the Mongo collections. I\u0026rsquo;m then using functions to make the GET calls, for example, here is the function to GET a single species:\nfunc GetSpecies(c echo.Context) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) speciesName := c.Param(\u0026#34;speciesName\u0026#34;) var species models.Species defer cancel() err := speciesCollection.FindOne(ctx, bson.M{\u0026#34;name\u0026#34;: speciesName}).Decode(\u0026amp;species) if err != nil { return c.JSON(http.StatusInternalServerError, responses.Response{Status: http.StatusInternalServerError, Message: \u0026#34;error\u0026#34;, Data: \u0026amp;echo.Map{\u0026#34;data\u0026#34;: err.Error()}}) } return c.JSON(http.StatusOK, responses.Response{Status: http.StatusOK, Message: \u0026#34;success\u0026#34;, Data: \u0026amp;echo.Map{\u0026#34;species\u0026#34;: species}}) } We start by setting a context and cancel var which essentially primes our call to Mongo and gives it a 10 second timeout. Next give our species name a variable and then pass our species data model to the species variable. Then we defer our cancel.\nerr is then assigned to our Mongo call and we move to our if statement. If our call to Mongo succeded (i.e. not nil) then we move to our return statement. If it\u0026rsquo;s nil and the call failed we return an error response.\nThis process is essentially the same when you make a GET request to retrieve all of something, like species. See below\u0026hellip;\nfunc GetAllSpecies(c echo.Context) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) var species []models.Species defer cancel() results, err := speciesCollection.Find(ctx, bson.M{}) if err != nil { return c.JSON(http.StatusInternalServerError, responses.Response{Status: http.StatusInternalServerError, Message: \u0026#34;error\u0026#34;, Data: \u0026amp;echo.Map{\u0026#34;data\u0026#34;: err.Error()}}) } defer results.Close(ctx) for results.Next(ctx) { var singleSpecies models.Species if err = results.Decode(\u0026amp;singleSpecies); err != nil { return c.JSON(http.StatusInternalServerError, responses.Response{Status: http.StatusInternalServerError, Message: \u0026#34;error\u0026#34;, Data: \u0026amp;echo.Map{\u0026#34;data\u0026#34;: err.Error()}}) } species = append(species, singleSpecies) } return c.JSON(http.StatusOK, responses.Response{Status: http.StatusOK, Message: \u0026#34;success\u0026#34;, Data: \u0026amp;echo.Map{\u0026#34;data\u0026#34;: species}}) } Here we\u0026rsquo;re doing a Find instead of FindOne in our call to Mongo. Then we\u0026rsquo;re taking each result (or species here) and formatting the data to the model and appending it into what tends to be a very large JSON result.\nConclusion # So that\u0026rsquo;s a mostly terse explanation of how this works. I plan to make more improvements over time like adding in a secure way for me to do PUT/DELETE/UPDATE calls, better improve errors since regardless of the type of error they all come back a 500 internal server error and in time, hopefully a web app that can utilize this api to create WOIN characters.\nIn part 4 of this series I\u0026rsquo;ll conclude with putting this API on the AWS lightsail instance, setting it up as a auto-starting app in systemd, getting ssl enabled and a url so that calls can be made. Tune in next time ;)\n","date":"January 28, 2023","externalUrl":null,"permalink":"/post/woin-api-pt-3/","section":"Posts","summary":"In part 2 of this series I laid out the foundation for this project as well as the issues and solutions I decided on. So now lets actually write some code.\ngo mod init foo # I first made a Github repo, cloned it locally and ran go mod init to start. I did some searching on directory structure and organization for an API with the intent to not end up with a monolith of a main.go file. Since I’ll be breaking this API into modules I settled on the following…\n","title":"Let's Build An API","type":"post"},{"content":" Requirements # In the previous post, I mentioned I want to create this API using Go so I\u0026rsquo;ll need a web framework, and since I\u0026rsquo;m planning to return JSON when calls are made I\u0026rsquo;ll need a NoSQL database solution.\nAlso, since I don\u0026rsquo;t want to write all the stats and information for all of WOINs data I\u0026rsquo;ll need to get it from somewhere.\nSome Quick Background on Me # I\u0026rsquo;m a hobbyist-level programmer at best. I\u0026rsquo;ve mainly learned what I needed, on the fly mind you, out of interest or to solve a specific problem for work. If you look at my code and wonder, \u0026ldquo;why would he do this?\u0026rdquo; you now know why. Also, my knowledge of Go when starting this project consisted of completing the Go track on Code Academy and writing a small app to text me if the freezer in my garage loses power, nothing fancy, and nothing overly complex.\nThe Database Problem # The problem is I don\u0026rsquo;t know much about them outside of basic knowledge like the difference between relational databases and NoSQL, what you\u0026rsquo;d store in each, and some basic querying. A DBA I am not and the idea of setting one up locally just to get this off the ground made me sad. So off to the interwebs I went hoping to find a solution. I looked at the managed services offerings from Couchbase and MongoDB and while it looks like Couchbase has some better features at scale, Mongo has a free forever tier, and well, pricing wins out here. Setup was easy and their desktop UI makes it easy to create collections and query them.\nSetup also allows you to choose which cloud provider you want your mongo instance in. I chose AWS because once the API is working I plan to build and run it in an AWS lightsail instance. Why lightsail? It\u0026rsquo;s a VPS in the same region as my database and it comes with some decent features and a friendly price tag of $3.50/month with the first 3 months free.\nSure I could use one of the raspberry pis I have lying around but I like the idea of the API and DB running in the same AWS region and also not having to maintain the pi, open ports for incoming traffic at home, or deal with power outages, internet outages, etc. and $3.50/month is a budget-friendly price point.\nSourcing WOIN Data # To my knowledge, there is no preexisting API or lists of data that exist as files of some format somewhere. I searched the interwebs and even the official EN Publishing site and associated forums and managed to find some web pages with tables of data relating to species, origins, careers, career/racial exploits and, universal exploits.\nThey are just HTML tables on a page though and trying to copypasta these will get you data but it\u0026rsquo;s not pretty. Take the origins page for example. Certain fields are nil rather than having a 0 value. There are rows in the first column resembling ***foo*** that serve as headers for the following row(s) which could use some cleanup as well. Not the end of the world and can likely be fixed in bulk with a find and replace. So, how do we get the data?\nWell, I know enough about python to know it can do web scraping so naturally I googled and settled on Selenium. Then I spent some time RTFM and figured I could make it happen pretty easily. Now I only needed a webdriver and some python code.\nI scraped these into CSV files because those are easy to edit, find/replace I mentioned earlier, and they are a method of upload into MongoDB.\nfrom selenium import webdriver import csv driver = webdriver.Chrome(executable_path=\u0026#39;/path/to/chromedriver\u0026#39;) driver.implicitly_wait(10) # wait for the page to load driver.maximize_window() # self explanatory urls = [\u0026#39;\u0026#39;] for url in urls: driver.get(url) # Scrape data t_rows = driver.find_elements_by_tag_name(\u0026#39;tr\u0026#39;) # tr is HTML for \u0026#39;table row\u0026#39; row_data = [line.text for line in t_rows] row_splits = [line.split() for line in row_data] with open(\u0026#39;foo.csv\u0026#39;, \u0026#39;a\u0026#39;, newline=\u0026#39;\u0026#39;) as f: line_writer = csv.writer(f) line_writer.writerows(row_splits) Each page was scraped into its own file and uploaded to Google Drive. Then began the longest and most boring part of this project, cleaning up the data. The find and replace wasn\u0026rsquo;t too hard. It was the text formatting that existed in some cells where text would wrap unnecessarily and needed to be cleaned by hand.\nSome things like careers or exploits also have prerequisites listed in the rule books that aren\u0026rsquo;t listed in these tables. I added those where I could as I don\u0026rsquo;t have all the rule books and in the case of Judge Dredd and the Worlds of 2000 A.D., it\u0026rsquo;s no longer available. I\u0026rsquo;m hoping to buy what I\u0026rsquo;m missing soon and hopefully find the information I\u0026rsquo;m missing where things no longer exist.\nWith the data now scraped, cleaned, and, formatted I simply created a collection for each in Mongo and imported them. Easy peasy.\nPicking a Go Web Framework # When I originally thought of this project I planned to write it in Python as that\u0026rsquo;s what I have the most experience with and I at least knew of Flask and that it would work fine here.\nI didn\u0026rsquo;t have that existing knowledge with Go so off to Google I went and lo and behold there\u0026rsquo;s a simple tutorial on the official Go website on making a RESTful API with Go and Gin.\nThe tutorial is easy to follow and nicely covers the CR of a CRUD app but just like with my DB choice I wanted to see what else may be out there in terms of a web framework.\nI ended up finding echo which has some nice features I\u0026rsquo;d like to implement in the future and it boasts some nice benchmarks when compared to Gin. Their docs are nicely laid out and have some great examples too including one for CRUD.\nConclusion # I got my data, my DB, and my web framework. In the next installment, I\u0026rsquo;ll start building the API. See you then.\n","date":"August 26, 2022","externalUrl":null,"permalink":"/post/woin-api-pt-2/","section":"Posts","summary":"Requirements # In the previous post, I mentioned I want to create this API using Go so I’ll need a web framework, and since I’m planning to return JSON when calls are made I’ll need a NoSQL database solution.\nAlso, since I don’t want to write all the stats and information for all of WOINs data I’ll need to get it from somewhere.\n","title":"Beginning the W.O.I.N. API","type":"post"},{"content":"","date":"August 26, 2022","externalUrl":null,"permalink":"/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":"I\u0026rsquo;m pretty obsessed with a TTRPG system called WOIN (What\u0026rsquo;s Old is New). I first learned about it from my friend Mike who described it as a more open system than DnD 5e that included canon for creating and/or playing in any world/campaign from high fantasy to space opera. The idea of playing a Cyberpunk themed campaign appealed to me at the highest levels because it\u0026rsquo;s a genre I\u0026rsquo;ve always been enamored with. One of my favorite games is Shadow Run for Sega Genesis and the thought of a sandbox dystopian cyberpunk world was too good not to try as a first-time TTRPG player.\nTurns out it is massively fun to play and I quickly began looking for online resources for character creation. There are two available via the official site and, in my opinion, work just fine as is IF you have a rule book in front of you and are aware that some of the decisions you make regarding your character have prerequisites. The prerequisites aren\u0026rsquo;t made clear during the character creation process on these sites which isn\u0026rsquo;t a huge deal unless you or your GM is a stickler for rules.\nThat said, I\u0026rsquo;ve been wanting to create something in Go for a while. It\u0026rsquo;s a language I\u0026rsquo;ve been wanting to learn and traditional methods like watching videos or building small apps like a calculator don\u0026rsquo;t do well for me. I need to care about what I\u0026rsquo;m creating or else I have no desire to pay attention to what I\u0026rsquo;m \u0026ldquo;learning\u0026rdquo;. It\u0026rsquo;s a weird idiosyncrasy of mine but it works and like a lot of tech people, I have started and abandoned many projects in the name of learning. My last project has spanned a few years and it\u0026rsquo;s still running despite me rarely updating the blog. It was fun, I learned a lot and that little forex bot is still running. I started with $1000 in a practice account and the current value sits at $799.10 which I think is pretty good for being completely hands-off and running since June of 2020. Down $200.90 after all the craziness of covid through now is pretty damn good considering most trading algorithms go broke quickly. I\u0026rsquo;ll never be rich but I learned a lot about forex and Python.\nThis brings me back to Go and WOIN. I had the idea of creating an API for WOIN and all the data needed to create a character. Then I found this, which is for D\u0026amp;D only but you see where I\u0026rsquo;m going with this. With an API built around verbose data, it would be easy to make whatever WOIN-related web app you wanted.\nSo that\u0026rsquo;s the project, create an API for this TTRPG system I love. I don\u0026rsquo;t expect it to get much traction outside of its community, maybe not even inside, but I\u0026rsquo;ll learn some Go and how to build an API along the way, and in the end that\u0026rsquo;s all I care about.\nThis will be an ongoing series of posts as I go through collecting the data, getting it into a DB, and then building the API in Go. Hint I\u0026rsquo;ve already started so the next post will be coming soon.\n","date":"August 14, 2022","externalUrl":null,"permalink":"/post/building-a-woin-api/","section":"Posts","summary":"I’m pretty obsessed with a TTRPG system called WOIN (What’s Old is New). I first learned about it from my friend Mike who described it as a more open system than DnD 5e that included canon for creating and/or playing in any world/campaign from high fantasy to space opera. The idea of playing a Cyberpunk themed campaign appealed to me at the highest levels because it’s a genre I’ve always been enamored with. One of my favorite games is Shadow Run for Sega Genesis and the thought of a sandbox dystopian cyberpunk world was too good not to try as a first-time TTRPG player.\n","title":"Building a W.O.I.N. API","type":"post"},{"content":"","date":"July 30, 2022","externalUrl":null,"permalink":"/tags/gcp/","section":"Tags","summary":"","title":"GCP","type":"tags"},{"content":"","date":"July 30, 2022","externalUrl":null,"permalink":"/tags/gke/","section":"Tags","summary":"","title":"GKE","type":"tags"},{"content":"","date":"July 30, 2022","externalUrl":null,"permalink":"/tags/istio/","section":"Tags","summary":"","title":"Istio","type":"tags"},{"content":"","date":"July 30, 2022","externalUrl":null,"permalink":"/series/multicluster/","section":"Series","summary":"","title":"Multicluster","type":"series"},{"content":"","date":"July 30, 2022","externalUrl":null,"permalink":"/tags/multicluster/","section":"Tags","summary":"","title":"Multicluster","type":"tags"},{"content":"The Github repo containing files discussed or shown in this post can be found here.\nAssumptions and Requirements # Before beginning, I\u0026rsquo;ll go over some requirements and I\u0026rsquo;m also going to make some assumptions about you, the reader, and the environment you\u0026rsquo;re working from.\nAssumptions # I\u0026rsquo;m going to first assume that you\u0026rsquo;ve got a base level of knowledge and experience with GKE, Istio, and Kubernetes in general.\n\u0026#9888; Be aware of the potential cost involved in creating the resources mentioned herein. Cloud resources are not free. These were not created on any sort of free tier.\nI'm also going to assume you know how to create your own GKE clusters in your preferred manner. Requirements # You\u0026rsquo;ll need two GKE clusters for this, preferably in different regions. In this blog post, I\u0026rsquo;m using clusters in us-east1 and us-central1. These clusters must be running Kubernetes 1.21+ and will need the below-listed items completed before proceeding.\nHTTP Load Balancing, Workload Identity, and Node Auto Provisioning need to be enabled on the clusters. These can be enabled via the GCP UI or IaC options. I used private GKE clusters for this project and as such there are some firewall changes to make as mentioned here in the For private GKE clusters box. I have Control plane global access enabled on my clusters which seems to render the section on Multicluster communication moot in my case. If you experience issues you should follow those instructions to see if they remedy them. You\u0026rsquo;ll also need to install istioctl which is available here and also via Homebrew. Setting Up Multicluster Istio Service Mesh # \u0026#9432; I'm using Istio version 1.14.1 in this post. You may need to change the file version numbers depending on the version you're using.\nRequirements # First, create an istio-system namespace in both clusters so our istio resources will have a place to live, kubectl create ns istio-system.\nMulticluster Istio has its own requirements before installing/configuring multicluster. The most important part of this is the certs. Ideally, you have access to a CA so you can issue intermediate certs for each of your clusters so workload traffic can be authenticated.\nSince this is a PoC I\u0026rsquo;m just using the Istio plugin certs made with the scripts in their repo.\n\u0026#9888; It should go without saying but don't run this setup in a production env using the istio CA certs used in the above process.\nSimply follow the directions to create the kube context remote secrets and get the certs installed.\nInstalling Istio # I find the easiest way to install and manage istio is via the operator. Per the Istio documentation it\u0026rsquo;s not the preferred way to install/manage Istio but it\u0026rsquo;s not a deprecated method of installation and in my experience, if something seems off with Istio it\u0026rsquo;s an easy thing to simply kick the istio operator pod and let it check, and possibly correct, all associated resources when it comes up. I also find the operator easier to use when upgrading istio.\nI\u0026rsquo;m using the multi-primary install here so both of my clusters reside in the same VPC but not subnet or region. I\u0026rsquo;ve prepared Istio CRDs for each regional cluster. Both have the necessary settings detailed in the multicluster documentation along with a lot of other settings which are detailed in the operator options and global mesh options documentation.\n\u0026#9432; I'll detail some of the other options in the CRD at the end of this article and explain why I'm using them.\nSo, starting with the cluster in us-central1 and the corresponding CRD we\u0026rsquo;ll install the istio operator with istioctl operator init -f istio-operator-1-14-1.yaml -r 1-14-1. I\u0026rsquo;m using the -r flag here to tag this install with the revision 1-14-1. I use revision tags as it makes upgrading istio easier. I\u0026rsquo;ll cover upgrades in another post.\nThe installation will take a couple of minutes to settle. It will install all the needed things and it will provision an internal layer-4 network load balancer for the istio eastwest-gateway. I chose internal because well, this should be an internal-only load balancer in this instance.\nIf you need an external load balancer here because your clusters are in different VPCs with no direct route between them for whatever reason, you\u0026rsquo;d need to remove the service annotation here and I\u0026rsquo;d recommend you at least use an authorization policy to restrict ingress. However, with properly configured route tables, firewall/sec group rules, or even a VPN you can likely make internal load balancers talk across different VPCs or even other cloud providers.\nLastly, we need to apply the cross-network-gateway in the istio-system namespace. This exposes all services, (*.local), on the east-west gateway in both clusters. This gateway file exists in both directories in the repo and needs to be applied to both clusters.\nNow repeat the same operator init in the us-east1 cluster using the istio operator CRD for us-east1 and make sure you create the gateway too. If you\u0026rsquo;ve followed everything thus far, the remote kube contexts, the certs, and applied the CRDs in the right clusters you should now have both clusters using the same service mesh. You can verify if everything is working correctly by following these steps.\nSetting up Multicluster Ingress # With our multicluster service mesh up and running we\u0026rsquo;ll now install our multicluster ingress. First, we need a global IP address. You can create this with the following gcloud command; gcloud compute addresses create your-cool-ip-name --global. We\u0026rsquo;ll only refer to the IP address in code by the name you gave it but if you prefer to know the IPv4 address you can run gcloud compute addresses list to see it.\nNow you\u0026rsquo;ll follow the instructions to register your clusters to a fleet and specify a config cluster\n\u0026#9432; I chose the us-east1 cluster as my config cluster so the dir of the same name in the repo is where you'll find the files described below.\nLet\u0026rsquo;s start by creating our ingress. Edit the annotation on line 8 in the multi-cluster-ingress file with the name of the global IP you created earlier. You can also edit the TLS secret name on line 17 if you want a TLS cert associated with the lb, LetsEncrypt is a good option for this. Now you can kubectl apply this file and in a couple of minutes, the load balancer will be up.\nHere\u0026rsquo;s where we stray from the documentation to make this work specifically with our istio multicluster mesh. GKE multicluster ingress assumes you\u0026rsquo;ll make a multicluster service for every service in your config cluster that you want to be associated with this load balancer. Istio typically handles all ingress traffic through its istio-ingressgateway service and pod(s). To funnel traffic from the load balancer into these pods we need to create a multi-cluster-service that points to them and it needs to look almost exactly like the existing istio-ingressgateway service as seen here. You can kubectl apply this file as is.\nLastly, we\u0026rsquo;ll want to apply the ingress-frontendconfig.yaml and ingress-backendconfig.yaml to both clusters. The former redirects all inbound traffic on the load balancer to port 443 and the latter establishes node group health checks on our load balancer.\nVerifying External Traffic Hits Both Clusters # In each regional directory is a verify.yaml file which, when applied, creates all the necessary pieces for you to test both the ingress and the multi-cluster service mesh. Apply the manifest to both clusters, open your IP in a browser and you should see the Nginx page which will give you the pod name where the traffic was routed and if you tick the radio box to refresh the page constantly you\u0026rsquo;ll see that the pod name changes because your requests are being routed to both clusters over the eastwest-gateway.\nConsider checking out Locality Load Balancing for handling service issues in your mesh.\nRouting External Traffic to a Specific Cluster # Since I run a Prometheus stack in my cluster I also run Alertmanager. Alertmanager operates in what\u0026rsquo;s called a gossip cluster meaning that all Alertmanager instances are (should be) in communication with each other so that alerts aren\u0026rsquo;t duplicated and that HA instances are aware if one of them stops responding.\nBecause of GCPs multi-cluster ingress feature of always routing requests to the geographically closest cluster this creates issues with alertmanager. The resolution here is to give each alertmanager instance it\u0026rsquo;s own DNS record and then incorporate destination rule subsets. This will still utilize the east-west gateway but allows them to be reached statically.\nIstio CRD Options Explained # Here I\u0026rsquo;ll discuss some of the more opaque settings in the istio CRD files. The majority of settings can be grokked via the documentation and the files are almost identical save for the geographical specific settings however some are specific to GKE, issues I\u0026rsquo;ve encountered, or what I consider best practice for Istio.\nI\u0026rsquo;ll start with the below snippet. Here I\u0026rsquo;m setting a 100% sample rate for traces and pointing it to my Jaeger installation. I think tracing is an important tool in Kubernetes clusters and when using Istio, creating a stack of Jaeger, Kiali, Grafana, and Prometheus can help you debug issues in your mesh, with your services, visualize metrics and data and also help you monitor and alert on them.\ntracing: sampling: 100 # Default is 1% zipkin: address: \u0026#34;jaeger-collector:9411\u0026#34; Now\u0026rsquo;s a good time to link information on federating Prometheus in a multicluster setup.\nThe below is sort of self-explanatory thanks to the comment but what isn\u0026rsquo;t clear is the telemetry resource I mention and how it relates to this.\nextensionProviders: # This was required along with an istio telemetry resource to see stackdriver logs in GCP - name: stackdriver stackdriver: maxTagLength: 256 I included the telemetry file in the repo. It\u0026rsquo;s worth noting that out of the box, Istio will work with Prometheus but this file is needed if you want logs/metrics to go to stackdriver in GCP. We needed to specify this so we could test out alerting and SLOs in GCP.\napiVersion: telemetry.istio.io/v1alpha1 kind: Telemetry metadata: name: mesh-default namespace: istio-system spec: # no selector specified, applies to all workloads metrics: - providers: - name: stackdriver - name: prometheus One difference in the files is an annotation on the istio-ingressgateway in us-central1.\ncloud.google.com/neg: \u0026#39;{\u0026#34;exposed_ports\u0026#34;: {\u0026#34;443\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;us-central1\u0026#34;}}}\u0026#39; The above differs from its us-east1 counterpart and does so because I\u0026rsquo;ve set us-east1 as the config cluster. As such, us-central1 needs its own NEG (Network Endpoint Group) which is how ingress delineates this as a separate cluster for node-group health checking. I\u0026rsquo;m only exposing 443 here since our redirect makes 80 useless and I\u0026rsquo;ve given it a self-explanatory name.\nFinally, you\u0026rsquo;ll notice that all of the Istio resources, save for the operator, have a corresponding pdb, hpa, nodeSelector, toleration, and pod antiAffinity configured. I put everything on preemptive nodes for cost savings and I ensure that these pods are always available via the aforementioned configurations.\nConclusion # So there you have it, multicluster service mesh with multicluster ingress in GKE. Now that I\u0026rsquo;ve written it all out it doesn\u0026rsquo;t seem that difficult to do but when I started looking into istio in GKE in a single cluster with an ingress there wasn\u0026rsquo;t much documentation on how to make it all work that wasn\u0026rsquo;t just \u0026ldquo;use Anthos\u0026rdquo;. I spent a few weeks trying and failing to get the right mix of things to work so I hope this helps you down whatever reliability path you\u0026rsquo;re on.\nIf you have questions about this post, message me on LinkedIn.\n","date":"July 30, 2022","externalUrl":null,"permalink":"/post/multicluster-two/","section":"Posts","summary":"The Github repo containing files discussed or shown in this post can be found here.\nAssumptions and Requirements # Before beginning, I’ll go over some requirements and I’m also going to make some assumptions about you, the reader, and the environment you’re working from.\nAssumptions # I’m going to first assume that you’ve got a base level of knowledge and experience with GKE, Istio, and Kubernetes in general.\n","title":"Multicluster Part Deux","type":"post"},{"content":"","date":"July 30, 2022","externalUrl":null,"permalink":"/categories/work/","section":"Categories","summary":"","title":"Work","type":"categories"},{"content":"This is Part 1 of a two part series.\nAs part of the SRE team at Harness, the goal is to help increase stability and reliability for our org while keeping in mind the need to scale as we grow. Here I\u0026rsquo;ll explain where we were with our platform, how we changed some things, for the better, in our move from AWS to GCP, and how we think we can help improve things here at Harness.\nA Brief History of the ZeroNorth Platform # ZeroNorth was acquired by Harness in November 2021 and I joined six months before to help bring an aging, poorly constructed monolith of a platform into the now. Running EKS version 1.14 with no autoscaling, no service mesh, manually applied manifests/resources, insufficient monitoring and alerting. Saying it was in bad shape is putting it lightly.\nThe plan we formulated was to spin up a new cluster on the latest version of EKS, by establishing IaC with Terraform, adding in ArgoCD, Istio, and the other services we saw as standard for EKS clusters, e.g. Autoscaler, ExternalDNS, Prometheus Stack, Grafana, Kiali, and Jaeger.\nThe result was a completely overhauled platform that allowed our devs to push and, if necessary, roll back releases on their own with monitoring and alerting tools that enabled us to respond to issues without being notified first by customers.\nMy Time at Harness So Far # Harness is heavily invested in both GCP and Kubernetes and this was my, as well as my co-workers, first foray into GCP. While getting situated in our new environment, we quickly noticed areas where we could lend our expertise in helping scale Harness to the next level.\nThe current platform, concerning infrastructure, is perfectly cromulent and has served Harness well over time. However, I don’t believe it is sufficient to take us to the next level.\nArchitecting a Better Platform # I took on the idea of presenting a PoC for a multi-regional platform utilizing a feature from each, Istio and GCP. The intent is to have multiple clusters in different regions, and potentially globally, sharing traffic internally as a single istio service mesh with ingress being managed by GCP\u0026rsquo;s multicluster ingress feature. The hypothetical result is a platform that exists across regions and if a cluster failed or a region disappeared, failover would be automatic and platform outages greatly reduced.\nIstio Multicluster # Using the Istio service mesh in your Kubernetes cluster has inherent benefits and when you incorporate its ability to connect multiple, geographically dispersed clusters in a single service mesh you set yourself up with the ability to create highly available services.\nIstio, in a multicluster mesh configuration, makes use of an east/west load balancer between all clusters in its mesh. The gateway resource attached to this load balancer uses TLS certs for authentication. Clusters communicate with each other over these east/west load balancers by making API calls to the other clusters. This requires each cluster to have the kube context of all other clusters loaded into a secret via the istioctl x create-remote-secret command.\nGKE Multicluster Ingress # GKE\u0026rsquo;s multicluster ingress creates a globally available, layer 7 http/s load balancer with an Anycast VIP. It uses a single IP address, performs proximity-based routing, monitors the clusters in its fleet for health, and handles failover automatically. Latency is reduced via the Anycast VIP which is created in every PoP (point-of-presence) and routed accordingly as pictured below. GKE Multicluster Ingress Limitations # Multi-cluster ingress has some requirements and limitations as well as default quotas to keep in mind. Below are the ones of concern.\nYou can only deploy one instance of Multi Cluster Ingress per project.\nYou must turn off mTLS on your application because mTLS is not supported for L7 external load balancers. Note that this does not interfere with mTLS inside the istio mesh but would prevent mTLS with external clients.\nSNI is not supported by GCLB.\nNode Auto-Provisioning # The final part of this equation is utilizing Node Auto Provisionsing(NaP) to automatically create and scale node groups on our behalf as needed as well as right-sizing node types based on resource requests and limits. While spinning up our new clusters we noticed NaP which allowed us to skip making node groups via IaC and simply let GCP spin them up for us based on the requests of what\u0026rsquo;s scheduled in the cluster. Further, by adding nodeSelector to our resources we\u0026rsquo;re able to get things scheduled onto pre-emptive or spot nodes. With tolerations, NaP will automatically taint nodes for us. This means we can provision nodes as needed via YAML.\nConclusion # With the above components, I believe we can achieve a platform that scales on-demand, is highly available, and, fault-tolerant. With multi-mesh and ingress, we can also add on other clusters globally as needed which thanks to the Anycast routing, can reduce latency to customers.\nIn the next post, I\u0026rsquo;ll detail my PoC along with code, gotchas, and issues I\u0026rsquo;ve experienced.\n","date":"July 24, 2022","externalUrl":null,"permalink":"/post/multicluster/","section":"Posts","summary":"This is Part 1 of a two part series.\nAs part of the SRE team at Harness, the goal is to help increase stability and reliability for our org while keeping in mind the need to scale as we grow. Here I’ll explain where we were with our platform, how we changed some things, for the better, in our move from AWS to GCP, and how we think we can help improve things here at Harness.\n","title":"Multicluster Madness","type":"post"},{"content":"","date":"July 21, 2022","externalUrl":null,"permalink":"/tags/first/","section":"Tags","summary":"","title":"First","type":"tags"},{"content":"Hello World! and all that. Working on a post about my exploration in platform reliability/scalability using Istio multicluster service mesh and GCP\u0026rsquo;s multicluster ingress feature.\nWith any luck it\u0026rsquo;ll be interesting and not poorly written\u0026hellip;\n","date":"July 21, 2022","externalUrl":null,"permalink":"/post/hi/","section":"Posts","summary":"Hello World! and all that. Working on a post about my exploration in platform reliability/scalability using Istio multicluster service mesh and GCP’s multicluster ingress feature.\nWith any luck it’ll be interesting and not poorly written…\n","title":"Hi","type":"post"},{"content":" Hi, I\u0026rsquo;m Robert. I\u0026rsquo;ve been working in tech since 2006 in all sorts of roles. I live in southeast Michigan with my wife and have four wonderful kids. When I have the free time I enjoy working out, spending time with family, painting miniatures and wishing I had more time to plan and play TTRPGs like D\u0026amp;D.\n","externalUrl":null,"permalink":"/about/","section":"Robert Mendal","summary":" Hi, I’m Robert. I’ve been working in tech since 2006 in all sorts of roles. I live in southeast Michigan with my wife and have four wonderful kids. When I have the free time I enjoy working out, spending time with family, painting miniatures and wishing I had more time to plan and play TTRPGs like D\u0026D.\n","title":"About Me","type":"page"}]