<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: garry</title>
    <description>The latest articles on DEV Community by garry (@grrywlsn).</description>
    <link>https://dev.to/grrywlsn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F367177%2Fdd709c0d-9bb8-4e46-8add-1f6cab467ab0.jpg</url>
      <title>DEV Community: garry</title>
      <link>https://dev.to/grrywlsn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/grrywlsn"/>
    <language>en</language>
    <item>
      <title>Self-service infrastructure as code</title>
      <dc:creator>garry</dc:creator>
      <pubDate>Tue, 12 Mar 2024 15:53:02 +0000</pubDate>
      <link>https://dev.to/grrywlsn/self-service-infrastructure-as-code-23bl</link>
      <guid>https://dev.to/grrywlsn/self-service-infrastructure-as-code-23bl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;how to give product teams the autonomy to quickly provision infrastructure for their services, without the platform team losing control&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
   context 📚
&lt;/h2&gt;

&lt;p&gt;For many tech startups and scale-ups, the technology team usually evolves into some sort of central Platform team(s) (aka "DevOps", "Infrastructure", or "SRE", maybe some "Dev Ex") and a bunch of Product teams, who are constantly working to build new features and maintain the core product for customers.&lt;/p&gt;

&lt;p&gt;Product teams want to release their work quickly, often, and reliably, but with the autonomy of not needing to wait for a Platform team to provision a database or other resource for them. &lt;/p&gt;

&lt;p&gt;The Platform team, however, wants to be comfortable and confident that all of the infrastructure the company is running is stable, securely configured, and right-sized to reduce overspending on their cloud costs. They'll also want to keep infrastructure as code, with tools like Terraform, which the Product teams might not care to use or learn.&lt;/p&gt;

&lt;p&gt;So how can the Platform team enable the Product teams to work efficiently and not be blocked, while not losing visibility or control over the foundations upon which they run?&lt;/p&gt;

&lt;h2&gt;
  
  
  first attempt at self-service infra 🧪
&lt;/h2&gt;

&lt;p&gt;I wrote in &lt;a href="https://dev.to/grrywlsn/disposable-kubernetes-clusters-2f44"&gt;my previous post&lt;/a&gt; about how the Platform team I worked on adopted GitOps and Helm to codify the deployment process, with the additional benefits of making deployments auditable and making Kubernetes clusters recoverable in disaster scenarios. &lt;/p&gt;

&lt;p&gt;Once that migration was completed, we wanted to enable the many Product teams to have the independence to set up new micro-services without our involvement or the need to raise any tickets.&lt;/p&gt;

&lt;p&gt;Our first attempt was to introduce other engineering teams to Terraform - the Platform team was already using it extensively with &lt;a href="https://terragrunt.gruntwork.io"&gt;Terragrunt&lt;/a&gt;, and using &lt;a href="https://www.runatlantis.io"&gt;Atlantis&lt;/a&gt; to automate &lt;code&gt;plan&lt;/code&gt; and &lt;code&gt;apply&lt;/code&gt; operations in a Git flow to ensure infrastructure was consistent. We'd written modules, with documentation, and an engineer would simply need to raise a PR to use the module and provide the right values, and Atlantis (once the PR was approved by Platform) would go ahead and set it up for them.&lt;/p&gt;

&lt;p&gt;To us, this felt light touch. Product engineers wouldn't have to learn Terraform (Platform would own and maintain the Terraform modules), they'd just need to learn how to use those modules with Terragrunt and apply them with Atlantis. We wrote up docs, recorded a show and tell, ... profit?&lt;/p&gt;

&lt;p&gt;Except not quite.&lt;/p&gt;

&lt;h2&gt;
  
  
  another tool to learn 🛠️
&lt;/h2&gt;

&lt;p&gt;While a few engineers did start to use this workflow, and they appreciated getting more hands-on instead of raising tickets to have it done for them at some undetermined future date, most engineers didn't.&lt;/p&gt;

&lt;p&gt;Whether it's a cultural thing, and teams don't want to care about the nuts and bolts under their service at runtime, or simply because they're on tight deadlines and don't have time to stop and work out Atlantis steps, ultimately, it &lt;em&gt;doesn't really matter&lt;/em&gt;. As a Platform team, we're an enablement team, and the Product teams are our customers. What we had built was not serving their needs.&lt;/p&gt;

&lt;p&gt;Given the team had already adopted GitOps and were familiar with deployments powered by &lt;a href="https://fluxcd.io/flux/components/helm/helmreleases/"&gt;Helm Releases&lt;/a&gt; and &lt;a href="https://fluxcd.io"&gt;Flux&lt;/a&gt;, we wanted to move the provisioning of the infrastructure to be part of the same process of creating the service and its continuous deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  infrastructure as code as GitOps 🚀
&lt;/h2&gt;

&lt;p&gt;We stumbled upon a project for maintaining Terraform with CRDs that we could deploy with Helm. That project is now called &lt;a href="https://github.com/flux-iac/tofu-controller"&gt;Tofu-Controller&lt;/a&gt; - another WeaveWorks project, so it integrated great with our existing Flux setup.&lt;/p&gt;

&lt;p&gt;What it meant is that Product engineers could provision a database for their service, or any other per-service infrastructure they needed, from the Helm Release they already used to configure their service at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: account-service
  namespace: dev
spec:
  chart:
    spec:
      chart: kotlin-deployment
      version: 2.0.33
  values:
    replicas: 2
    infrastructure:
      postgres: 
        enabled: true
        rdsInstance: account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above (simplified) example shows that &lt;code&gt;.Values.infrastructure.postgres.enabled&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;. When the Helm chart is being installed or updated, then a &lt;code&gt;Terraform&lt;/code&gt; resource will be templated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{- if eq .Values.infrastructure.postgres.enabled true }}
---
apiVersion: infra.contrib.fluxcd.io/v1alpha2
kind: Terraform
metadata:
  name: "postgres-{{ .Release.Name }}"
spec:
  interval: 2h
  approvePlan: auto
  tfstate:
    forceUnlock: auto
  destroyResourcesOnDeletion: false
  sourceRef:
    kind: GitRepository
    name: service-postgres
  backendConfig:
    customConfiguration: |
      backend "s3" {
        ...
      }
  vars:
    - name: service_name
      value: "{{ .Release.Name }}"
    - name: rds_instance_name
      value: "{{ Values.infrastructure.postgres.rdsInstance }}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, this code has been simplified for clarity, but there are a few things worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the &lt;code&gt;Terraform&lt;/code&gt; resource can be configured to automatically plan and apply, and even &lt;code&gt;forceUnlock&lt;/code&gt; some state locks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the 2 hour &lt;code&gt;interval&lt;/code&gt; means the operator will continually try to re-apply the Terraform regularly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the &lt;code&gt;backendConfig&lt;/code&gt; meant that the Terraform operator can share the remote state bucket with Atlantis; this is powerful since you can then reference across modules to get remote state outputs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;importantly for stateful resources like databases, you can set &lt;code&gt;destroyResourcesOnDeletion&lt;/code&gt; to false to avoid destroying data when you uninstall the helm chart&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;we can pass in the &lt;code&gt;vars&lt;/code&gt; as usual to the &lt;code&gt;service-postgres&lt;/code&gt; Terraform module; here we're passing in a name for the service (which maps to the database name and database user) and the name of the RDS instance on which to create it&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the above Helm chart was installed, it would create a CRD of the &lt;code&gt;Terraform&lt;/code&gt; kind, the operator will go and plan and apply the &lt;code&gt;service-postgres&lt;/code&gt; module with the &lt;code&gt;vars&lt;/code&gt; set as inputs. In this case, it'll create a database and user called &lt;code&gt;account-service&lt;/code&gt; on the &lt;code&gt;account&lt;/code&gt; RDS instance, and manage roles and grants, passwords, security group access, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  eventually consistent infrastructure ⏱️
&lt;/h2&gt;

&lt;p&gt;Of course, the Terraform resources might take a short while to set up, so the service will need to handle scenarios where its Terraform dependencies might not exist yet. In practice though, we were able to provision databases, service accounts, S3 buckets, Kafka topics, and more within a few seconds and the service's pods would simply restart until they existed.&lt;/p&gt;

&lt;p&gt;The Terraform operator will also continually apply the resources it's managing, so that helped us to avoid drift between what we expect to exist; it also fixes any situations where a user might manually change the infrastructure outside of code approvals.&lt;/p&gt;

&lt;h2&gt;
  
  
  split brain terraform 🧠
&lt;/h2&gt;

&lt;p&gt;It's important to point out that this workflow is only for any per-service infrastructure; each Helm Release would provision services just for itself, and be managed by the operator.&lt;/p&gt;

&lt;p&gt;The Platform team would continue to use Atlantis and the Terragrunt repo to manage the main cloud estate (VPCs, security groups, database instances, EKS, etc). The Platform team would also maintain the deployment Helm chart and the Terraform modules referenced by it.&lt;/p&gt;

&lt;p&gt;The per-service Terraform modules could reference the remote state of those managed by Atlantis since they shared the same remote state S3 bucket. With the example above, by passing in the name of the RDS instance from the Terraform resource in Kubernetes, the operator can pull outputs from the instance's remote state when it was set up by Atlantis, based on whatever was needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  next steps 🥾
&lt;/h2&gt;

&lt;p&gt;In many ways, this made the HelmRelease in the GitOps repo a source of truth for the deployment; already describing &lt;em&gt;how&lt;/em&gt; it should work at runtime, it was also now including some of its dependencies.&lt;/p&gt;

&lt;p&gt;With time, our goal was to abstract the YAML from the repo into a service catalog like Backstage, where it would be easy to say "I want to create a service called X, it needs Postgres and Kafka", and its entire setup, including some boilerplate code and its infrastructure, could go off and be created automatically.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Disposable Kubernetes clusters</title>
      <dc:creator>garry</dc:creator>
      <pubDate>Sat, 31 Oct 2020 16:23:36 +0000</pubDate>
      <link>https://dev.to/grrywlsn/disposable-kubernetes-clusters-2f44</link>
      <guid>https://dev.to/grrywlsn/disposable-kubernetes-clusters-2f44</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;An overview of how we manage Kubernetes clusters at &lt;a href="https://www.curve.com"&gt;Curve&lt;/a&gt; to allow for zero downtime upgrades while handling live Curve card transactions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  context 📚
&lt;/h2&gt;

&lt;p&gt;When I joined Curve as the Lead SRE in January 2019, Kubernetes was already being used in production to manage the many microservices (and few monoliths) that make up the Curve estate. Quite bravely at the time, Curve was also using &lt;a href="https://istio.io"&gt;Istio&lt;/a&gt; in production - well before it had wider (aka "stable") adoption.&lt;/p&gt;

&lt;p&gt;The clusters were being set up manually by &lt;a href="https://github.com/kubernetes/kops"&gt;Kops&lt;/a&gt;, and deployments happened with Jenkins and a bunch of scripts. This is &lt;em&gt;fine&lt;/em&gt; but ideally not something you want to use in a production setup; recreating the cluster, or even just tracking the current version of what's deployed in an environment is a slow and difficult task.&lt;/p&gt;

&lt;h2&gt;
  
  
  adopting GitOps 👩‍💻
&lt;/h2&gt;

&lt;p&gt;The first step in trying to tackle this mild chaos was to define the current reality of our clusters in one central location - and what better tool to track the state of something than Git. It's scalable, it's got a change history, and all of the Engineering team already know how it works.&lt;/p&gt;

&lt;p&gt;We dabbled briefly with &lt;a href="https://argoproj.github.io/argo-cd/"&gt;ArgoCD&lt;/a&gt; but settled eventually on &lt;a href="https://www.weave.works/oss/flux/"&gt;Flux, by WeaveWorks&lt;/a&gt;. Its simplicity, and ability to manage Helm charts effectively with the Helm Operator, was a winner for what we wanted to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  standardising deployments 🚀
&lt;/h2&gt;

&lt;p&gt;Before Flux, Jenkins managed deployments through a Git repo of its own, but templated in values from the repo with image versions at deploy time, which weren't committed back to the repo.&lt;/p&gt;

&lt;p&gt;Additionally, everything defined in that repo was just raw YAML; engineers would copy/paste config from an existing service to define a new one, often copying bits of config that weren't relevant to their new service.&lt;/p&gt;

&lt;p&gt;The Platform team started work on a Helm chart that would replace all of that - no more copy-pasting, just add your service name and the version of the image you want to deploy. &lt;/p&gt;

&lt;p&gt;A bunch of sensible defaults would be established (resource requests and limits, health checks, rollout strategy), and the Platform team would encourage standardisation of services (ports, metrics endpoints, and so on). &lt;/p&gt;

&lt;p&gt;Each service would be defined as a &lt;a href="https://docs.fluxcd.io/projects/helm-operator/en/1.0.0-rc9/references/helmrelease-custom-resource.html"&gt;Helm Release&lt;/a&gt;; an instantiation of that Platform-managed chart. Values could be added to a release to override some defaults or add optional features, such as setting up an ingress route from the Internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  rebuilding the infrastructure 🏗️
&lt;/h2&gt;

&lt;p&gt;With work underway to manage the services as code and standardise deployments with Helm, we began work on replacing Kops with clusters that were &lt;em&gt;also&lt;/em&gt; managed by code. We chose to move to &lt;a href="https://aws.amazon.com/eks/"&gt;AWS's EKS&lt;/a&gt;, which we'd set up and configure with &lt;a href="https://www.terraform.io"&gt;Terraform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Terraform module we wrote for EKS sets up the infrastructure of course — such as the EKS control plane, the worker nodes, security groups and some IAM roles — but also installs onto the cluster a few components we consider core - Terraform uses Helm to install Istio, &lt;a href="https://www.getambassador.io"&gt;Ambassador Edge Stack&lt;/a&gt; (our API gateway), and Flux with its Helm Operator.&lt;/p&gt;

&lt;p&gt;When the Terraform module is applied, a fully working cluster will start up, with an Istio service mesh, an API gateway with a load balancer for ingress, and Flux preconfigured to connect to the Git repo that defines what should be deployed on that cluster. Flux will take over deploying everything else not deployed by Terraform, including monitoring tools and all of our production services.&lt;/p&gt;

&lt;h2&gt;
  
  
  cycling through clusters ♻️
&lt;/h2&gt;

&lt;p&gt;Combining the easily Terraform-able EKS cluster, which would start up and deploy all of the services we defined in code with Helm, meant we could easily create, destroy and recreate our environments at will.&lt;/p&gt;

&lt;p&gt;That's great for dev environments where we can recreate the cluster often, but how do we upgrade production without causing downtime? Any outage of our services means we decline card transactions, upset customers, and cause the business to lose revenue. We need to do seamless upgrades.&lt;/p&gt;

&lt;p&gt;Like the old "&lt;a href="http://cloudscaling.com/blog/cloud-computing/the-history-of-pets-vs-cattle/"&gt;cattle not pets&lt;/a&gt;" mantra, we decided to treat each of our clusters as something disposable - rather than try risky in-place upgrades of Istio or other core components, we'd simply start a new cluster configured the way we want, and switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  making the switch 🚦
&lt;/h2&gt;

&lt;p&gt;The key to this was our simple cluster ingress - &lt;em&gt;all&lt;/em&gt; customer-facing calls to our APIs go through Ambassador Edge Stack across one load balancer. Each cluster has its own load balancer, set up by Terraform.&lt;/p&gt;

&lt;p&gt;We set the EKS Terraform module to output the DNS of the load balancer to remote state, and created another Terraform module to handle &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-values-weighted.html"&gt;weighted routing&lt;/a&gt; to those load balancers. This new module would create a fixed Route 53 entry with a CNAME that would resolve to a different load balancer address based on weighting we gave each.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  cluster_weighting = [
    "cluster-a" = "90",
    "cluster-b" = "10",
  ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For simplicity, we attribute a percentage value between clusters, and handle them as a map in Terraform. In the example above, CNAMES are created for &lt;code&gt;cluster-a&lt;/code&gt; and &lt;code&gt;cluster-b&lt;/code&gt;, and 90% of traffic would solve to &lt;code&gt;cluster-a&lt;/code&gt; and reach its load balancer. The fixed Route 53 record that served the weighted load balancer records was then used as the origin for the &lt;a href="https://aws.amazon.com/cloudfront/"&gt;CloudFront distribution&lt;/a&gt; that sits in front of our APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  going live 🚨
&lt;/h2&gt;

&lt;p&gt;We practiced this process &lt;em&gt;many&lt;/em&gt; times in our non-production environments before we made the switch in production, to the point where we had destroyed and recreated dozens of clusters in the months before we went live.&lt;/p&gt;

&lt;p&gt;In the end, &lt;a href="https://www.linkedin.com/posts/grrywlsn_yesterday-we-moved-all-of-curves-production-activity-6663749886594301952-fAL3"&gt;in a single day&lt;/a&gt; we moved all of our API traffic and all card payment transactions from our old Kops cluster to EKS, without dropping a single payment. We stepped up the percentage of traffic gradually at first with weighted routing until all traffic was migrated.&lt;/p&gt;

&lt;p&gt;This week we updated to the latest version of EKS and did the same process again, but this time we did the whole thing between morning standup and lunch. We're continuing to refine the process to the point that soon, we will make it fully automated. I'll post more on how that journey goes!&lt;/p&gt;

</description>
      <category>eks</category>
      <category>kubernetes</category>
      <category>aws</category>
      <category>sre</category>
    </item>
  </channel>
</rss>
