Akamai for autonomous teams - the DNB way!

By
  • Vegard Engen
Nov. 23 20228 min. read time

Observant users of the DNB online bank will already have seen that we are using Akamai as a CDN for a lot of our services. In this article I will try to give some insight into what a CDN is, how such a service can be of help in infrastructure modernization, and how an organization moving full speed ahead in the direction of agile, autonomous teams can co-manage configuration of Akamai.

When accessing our online bank, https://www.dnb.no/, you might think that you hit "our server". This is a highly simplified view. The picture below is more like how it works, but even this is a bit simplified.

The online netbank of course doesn't exist as separate copies, but there is a ton of elements on it - think images, static text, styling - that doesn't vary a whole lot. In a CDN, you'll try to store this as close to the user as possible. This will affect the speed and experience of using the systems. After all, getting your account balance is, a bit simplified, only a single number - the whole page and styling around it is much more.

In a CDN, you configure all of this - what can be cached and what needs to be fetched from the central server - in a policy. This policy will be pushed out to a whole set of servers in the Akamai cloud.

But wait a minute? Is it that simple? Is there only one central server? Of course not. Different parts of the site can be stored in different places. At the present day, we have a lot of our functionality in our own datacenter - but this is quickly changing! So even though it looks as it is only one server to you, in reality Akamai will look at the web request and might fetch it from totally different places.

So, how do you actually manage all of this? Maybe those creating retail systems have widely different requirements for caching etc than the corporate department?

This is where configuration management - and in modern times, configuration as code - enters the picture. All of this will be created in a ruleset that is checked into our code repository.

When we have some changes that we want to roll out, we run a CI/CD pipeline. Our pipeline is a piece of software that will take our code, translate it to Akamai native format, send it to Akamai and instruct Akamai to roll it out. Of course, we'll roll it out to a test system first, but when we have tested that it works in our test systems, we'll roll out the same code to our production system. All of this is can be done whenever we want and as often as we like - because that is what CDNs were built for!

But, enough talk! Time for an example. Below, I have an element that simply says to cache for 5 minutes. I will give a more complete example later, with (a slightly redacted) version of the configuration for https://www.dnbtech.no/

   {
      "name": "Caching",
      "criteria": [],
      "behaviors": [
        {
          "name": "caching",
          "options": {
            "behavior": "MAX_AGE",
            "ttl": "5m",
            "mustRevalidate": true
          }
        }
      ],
      children: [],
      "criteriaMustSatisfy": "any",
      "options": {}
    }
Caching configuration for https://www.dnbtech.no/

As you can see, this is all JSON code. It may look complex, but Akamai provides some good documentation at https://techdocs.akamai.com/property-mgr/reference/rule-trees - for those interested in diving down into what it actually does!

Of course, any site as large as https://www.dnb.no/ will have quite a lot of configuration. The different parts of the configuration will be managed by different teams - for example security related configuration might be managed by our security department.

JSON files can include other files, so the way we have structured our JSON code is that the outline of a web site can be handled by one team, while parts of it is delegated through others teams by including configs from team specific directories that are (almost) fully managed by them,. Now, since all of this configuration need to work together, and there is potential to destroy for other parts of the site, the final decision about whether to include the code will be handled by a smaller set of Akamai specialist.

For software developers, the process here will be known - contributors create code and submits pull requests that Akamai specialists will overlook - possibly spotting mistakes or suggesting improvements. The code in the pull request will also be automatically checked by our pipeline, making sure it's at least syntactically correct.

Once a pull request is approved, it will be merged, and the CI/CD pipeline will kick off another set of tests. Once they pass, we can roll out the resulting Akamai configuration through our CI/CD tools. All of this minimizes the risk of manual errors.

To tie together all Akamai configurations into a complete infrastructure, we are using Terraform. There is a ready-made Akamai Terraform Provider that we are using, with some scripting on the outside to make our CI/CD tool kick off the deployment to Akamai through terraform.

Below, I am including a mostly complete example of how this blog is configured in Akamai. I changed a few names, left out the more irrelevant parts, and simplified the configuration only slightly so that it is more understandable, but this is the basic Akamai configuration of https://www.dnbtech.no/ - done the DNB Akamai way.

resource "akamai_cp_code" "ourbackend-prod-ourcorp-net" {
 contract_id = var.contract_id
 group_id = var.group_id
 product_id = var.product_id
 name = "ourbackend.prod.ourcorp.net"
}
resource "akamai_edge_hostname" "www-dnbtech-no" {
 contract_id = var.contract_id
 group_id = var.group_id
 product_id = var.product_id
 edge_hostname = "www.dnbtech.no.edgekey.net"
 ip_behavior = "IPV4"
 certificate = "11111"
}
resource "akamai_property" "www-dnbtech-no" {
 name = "www.dnbtech.no"
 contract_id = var.contract_id
 group_id = var.group_id
 product_id = var.product_id
 hostnames {
  cname_from = "www.dnbtech.no"
  cname_to = "www.dnbtech.no.edgekey.net"
  cert_provisioning_type = "CPS_MANAGED"
 }
 hostnames {
  cname_from = "dnbtech.no"
  cname_to = "www.dnbtech.no.edgekey.net"
  cert_provisioning_type = "CPS_MANAGED"
 }
 rule_format = "v2022-06-28"
 rules       = data.akamai_property_rules_template.rules.json
}
resource "akamai_property_activation" "www-dnbtech-no" {
 property_id = akamai_property.www-dnbtech-no.id
 contact = ["firstname.lastname@somecompany.no"]
 version = akamai_property.www-dnbtech-no.latest_version
 network = upper(var.network)
 note = var.note
}
resources.tf - terraform properties
data "akamai_property_rules_template" "rules" {
 template_file = "./property-snippets/rules.json"
 variables {
   name = "default_deny"
   value = "false"
   type = "bool"
 }
 variables {
   name = "techblog_cpcode"
   value = replace(akamai_cp_code.ourbackend-prod-ourcorp-net.id,"cpc_","")
   type = "number"
 }
 variables {
   name = "config_name"
   value = var.config_name
   type = "string"
}
}
data.tf - terraform configuration
variable "contract_id"{
        default="ctr_P-XXXXXX"
}
variable "group_id"{
        default="grp_NNNNNN"
}
variable "product_id"{
        default="prd_Site_Accel"
}
variable "network"{
        default="staging"
}
variable "note"{
        default=""
}
variable "topdir"{
        default=""
}
variable "default_deny" {
        default="false"
}
variable "config_name" {
        default="www.dnbtech.no"
}
variable "properties_name" {
        default="www-dnbtech-no"
}
variable "hostnames" {
        default="www.dnbtech.no"
}
variables.tf - some variables that can be used in the Akamai configuration
{
  "rules": {
    "name": "default",
    "behaviors": [
      {
        "name": "origin",
        "options": {
          "cacheKeyHostname": "ORIGIN_HOSTNAME",
          "compress": true,
          "#include:team/blogteam/prod/dnb_ca.json",
          "customValidCnValues": [
            "{{Origin Hostname}}",
            "{{Forward Host Header}}"
          ],
          "enableTrueClientIp": true,
          "forwardHostHeader": "ORIGIN_HOSTNAME",
          "hostname": "{{user.PMUSER_TECHBLOG_ORIGIN}}",
          "httpPort": 80,
          "httpsPort": 443,
          "originCertificate": "",
          "originCertsToHonor": "COMBO",
          "originSni": true,
          "originType": "CUSTOMER",
          "ports": "",
          "standardCertificateAuthorities": [
            "akamai-permissive"
          ],
          "trueClientIpClientSetting": false,
          "trueClientIpHeader": "True-Client-IP",
          "verificationMode": "CUSTOM"
        }
      },
      {
        "name": "cpCode",
        "options": {
          "value": {
            "cpCodeLimits": null,
            "id": "${env.techblog_cpcode}",
            "products": [
                  "Site_Defender"
            ]
          }
        }
      },
      {
        "name": "caching",
        "options": {
          "behavior": "NO_STORE"
        }
      },
      {
        "name": "allowPost",
        "options": {
          "allowWithoutContentLength": false,
          "enabled": true
        }
      },
      },
      {
        "name": "report",
        "options": {
          "logAcceptLanguage": false,
          "logCookies": "OFF",
          "logCustomLogField": false,
          "logHost": false,
          "logReferer": false,
          "logUserAgent": true,
          "logEdgeIP": false,
          "logXForwardedFor": false
        }
      }
    ],
    "children": [
      "#include:team/blogteam/redirect_to_https.json",
      "#include:team/blogteam/prod/redirect_zone_apex_dnbtech.no.json",
      "#include:team/blogteam/prod/hsts_www.dnbtech.no.json",
      "#include:team/blogteam/prod/dce_rules_techblog.json"
    ],
    "variables": "#include:variables.json",
    "options": {
      "is_secure": true
    }
  }
}
property-snippets/rules.json - the main configuration!
["#include:team/blogteam/prod/dce_variables_techblog.json"];
variables.tf - only includes the variables from the blog team
{
  "name": "TechBlog",
  "criteria": [],
  "behaviors": [],
  "children": [
    {
      "name": "Caching",
      "criteria": [],
      "behaviors": [
        {
          "name": "caching",
          "options": {
            "behavior": "MAX_AGE",
            "ttl": "5m",
            "mustRevalidate": true
          }
        },
        {
          "name": "prefreshCache",
          "options": {
            "enabled": true,
            "prefreshval": 5
          }
        },
        {
          "name": "downstreamCache",
          "options": {
            "behavior": "MUST_REVALIDATE",
            "ttl": "0m",
            "sendPrivate": false
          }
        }
      ],
      "children": [
        {
          "name": "Cache static files for a long time",
          "criteria": [
            {
              "name": "path",
              "options": {
                "matchOperator": "MATCHES_ONE_OF",
                "normalize": true,
                "matchCaseSensitive": false,
                "values": ["/web/*.js", "/web/*.css", "/web/static/*"]
              }
            },
            {
              "name": "path",
              "options": {
                "matchOperator": "DOES_NOT_MATCH_ONE_OF",
                "normalize": true,
                "matchCaseSensitive": false,
                "values": ["/web/sw.js"]
              }
            }
          ],
          "behaviors": [
            {
              "name": "caching",
              "options": {
                "behavior": "CACHE_CONTROL_AND_EXPIRES",
                "defaultTtl": "1m",
                "enhancedRfcSupport": false,
                "honorMustRevalidate": false,
                "honorPrivate": false,
                "mustRevalidate": false
              }
            },
            {
              "name": "prefreshCache",
              "options": {
                "enabled": false
              }
            },
            {
              "name": "downstreamCache",
              "options": {
                "behavior": "ALLOW",
                "allowBehavior": "LESSER",
                "sendHeaders": "CACHE_CONTROL_AND_EXPIRES",
                "sendPrivate": false
              }
            }
          ],
          "criteriaMustSatisfy": "all",
          "options": {}
        }
      ],
      "criteriaMustSatisfy": "any",
      "options": {}
    }
  ],
  "criteriaMustSatisfy": "all",
  "options": {}
}
team/blogteam/prod/dce_rules_techblog.json - a very simple configuration that the Blog team can manage, only consisting of caching configuration for now.
      {
        "name": "PMUSER_TECHBLOG_ORIGIN",
        "value": "ourbackend.prod.ourcorp.net",
        "description": "",
        "hidden": false,
        "sensitive": false
      }
dce_variables_techblog.json - only contains the name of the backend endpoint in this simple configuration.
      {
        "name": "HSTS",
        "criteria": [
          {
            "name": "hostname",
            "options": {
              "matchOperator": "IS_ONE_OF",
              "values": [
                "www.dnbtech.no",
                "dnbtech.no"
              ]
            }
          }
        ],
        "behaviors": [
          {
            "name": "httpStrictTransportSecurity",
            "options": {
              "enable": true,
              "includeSubDomains": false,
              "maxAge": "ONE_YEAR",
              "preload": false,
              "redirect": true,
              "redirectStatusCode": 301
            }
          }
        ],
        "criteriaMustSatisfy": "all",
        "options": {}
      }
hsts_www.dnbtech.no.json - HSTS is always nice to have on a web site.
    {
        "name": "Redirect Zone Apex",
        "criteria": [
          {
            "name": "hostname",
            "options": {
              "matchOperator": "IS_ONE_OF",
              "values": [
                "dnbtech.no"
              ]
            }
          }
        ],
        "behaviors": [
          {
            "name": "redirect",
            "options": {
              "destinationHostname": "OTHER",
              "destinationHostnameOther": "www.dnbtech.no",
              "destinationPath": "SAME_AS_REQUEST",
              "destinationProtocol": "HTTPS",
              "mobileDefaultChoice": "DEFAULT",
              "queryString": "APPEND",
              "responseCode": 301
            }
          }
        ],
        "criteriaMustSatisfy": "all",
        "options": {}
      }
redirect_zone_apex_dnbtech.no.json - redirecting dnbtech.no to https://www.dnbtech.no/
          "customCertificateAuthorities": [
            {
              "subjectCN": "Amazon Root CA 1",
              "subjectAlternativeNames": [],
              "subjectRDNs": {
                  "C": "US",
                  "CN": "Amazon Root CA 1",
                  "O": "Amazon"
              },
              "issuerRDNs": {
                  "C": "US",
                  "CN": "Amazon Root CA 1",
                  "O": "Amazon"
              },
              "notBefore": 1432598400000,
              "notAfter": 2147299200000,
              "sigAlgName": "SHA256WITHRSA",
              "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsniAccp41eNxr0eAUHR9btjXiHb0mWj3WCFg+XSEAS+sAi2G06BDek6ypNA2ugG+jdtIyAcXNkz07ogjxz7rN/W1GfhJaLDe17l2OB1hnqT+gjal5UpW5EXh+f20Fvp02pybNTkv+rAgUAZsetCAsqb5r+xHGY9QOAfcooc5WPi61an5SGcwlu6UeF5viaNRwDCGZqFFZrpU66PDkflI3P/R6DAtfS10cDXXiCT3nsRZbrtzhxfyMkYouEP6tx2qyrTynyQOLUv3cVxeaf/qlQLLOIquUDhv2/stYhvFxx5U4XfgZ8gPnIcj1j9AIH8ggMSATD47JCaOBK5smsiqDQIDAQAB",
              "publicKeyAlgorithm": "RSA",
              "publicKeyFormat": "X.509",
              "serialNumber": "143266978916655856878034712317230054538369994",
              "version": 3,
              "sha1Fingerprint": "8da7f965ec5efc37910f1c6e59fdc1cc6a6ede16",
              "pemEncodedCert": "-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\nADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\nb24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\nA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\nU5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\nN+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\no/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\nrqXRfboQnoZsG4q5WTP468SQvvG5\n-----END CERTIFICATE-----\n",
              "canBeLeaf": true,
              "canBeCA": true,
              "selfSigned": true
            },
            {
              "subjectCN": "Amazon Root CA 1",
              "subjectAlternativeNames": [],
              "subjectRDNs": {
                  "C": "US",
                  "CN": "Amazon Root CA 1",
                  "O": "Amazon"
              },
              "issuerRDNs": {
                  "ST": "Arizona",
                  "C": "US",
                  "CN": "Starfield Services Root Certificate Authority - G2",
                  "L": "Scottsdale",
                  "O": "Starfield Technologies, Inc."
              },
              "notBefore": 1432555200000,
              "notAfter": 2145834000000,
              "sigAlgName": "SHA256WITHRSA",
              "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsniAccp41eNxr0eAUHR9btjXiHb0mWj3WCFg+XSEAS+sAi2G06BDek6ypNA2ugG+jdtIyAcXNkz07ogjxz7rN/W1GfhJaLDe17l2OB1hnqT+gjal5UpW5EXh+f20Fvp02pybNTkv+rAgUAZsetCAsqb5r+xHGY9QOAfcooc5WPi61an5SGcwlu6UeF5viaNRwDCGZqFFZrpU66PDkflI3P/R6DAtfS10cDXXiCT3nsRZbrtzhxfyMkYouEP6tx2qyrTynyQOLUv3cVxeaf/qlQLLOIquUDhv2/stYhvFxx5U4XfgZ8gPnIcj1j9AIH8ggMSATD47JCaOBK5smsiqDQIDAQAB",
              "publicKeyAlgorithm": "RSA",
              "publicKeyFormat": "X.509",
                            "serialNumber": "144918191876577076464031512351042010504348870",
              "version": 3,
              "sha1Fingerprint": "06b25927c42a721631c1efd9431e648fa62e1e39",
              "pemEncodedCert": "-----BEGIN CERTIFICATE-----\nMIIEkjCCA3qgAwIBAgITBn+USionzfP6wq4rAfkI7rnExjANBgkqhkiG9w0BAQsF\nADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNj\nb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4x\nOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1\ndGhvcml0eSAtIEcyMB4XDTE1MDUyNTEyMDAwMFoXDTM3MTIzMTAxMDAwMFowOTEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\nb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\nca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\nIFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\nVOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\njgSubJrIqg0CAwEAAaOCATEwggEtMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/\nBAQDAgGGMB0GA1UdDgQWBBSEGMyFNOy8DJSULghZnMeyEE4KCDAfBgNVHSMEGDAW\ngBScXwDfqgHXMCs4iKK4bUqc8hGRgzB4BggrBgEFBQcBAQRsMGowLgYIKwYBBQUH\nMAGGImh0dHA6Ly9vY3NwLnJvb3RnMi5hbWF6b250cnVzdC5jb20wOAYIKwYBBQUH\nMAKGLGh0dHA6Ly9jcnQucm9vdGcyLmFtYXpvbnRydXN0LmNvbS9yb290ZzIuY2Vy\nMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jcmwucm9vdGcyLmFtYXpvbnRydXN0\nLmNvbS9yb290ZzIuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQsF\nAAOCAQEAYjdCXLwQtT6LLOkMm2xF4gcAevnFWAu5CIw+7bMlPLVvUOTNNWqnkzSW\nMiGpSESrnO09tKpzbeR/FoCJbM8oAxiDR3mjEH4wW6w7sGDgd9QIpuEdfF7Au/ma\neyKdpwAJfqxGF4PcnCZXmTA5YpaP7dreqsXMGz7KQ2hsVxa81Q4gLv7/wmpdLqBK\nbRRYh5TmOTFffHPLkIhqhBGWJ6bt2YFGpn6jcgAKUj6DiAdjd4lpFw85hdKrCEVN\n0FE6/V1dN2RMfjCyVSRCnTawXZwXgWHxyvkQAiSr6w10kY17RSlQOYiypok1JR4U\nakcjMS9cmvqtmg5iUaQqqcT5NJ0hGA==\n-----END CERTIFICATE-----\n",
              "canBeLeaf": true,
              "canBeCA": true,
              "selfSigned": false
            }
          ],
          "customCertificates": []
dnb_ca.json - since we host our techblog backend in Amazon, including the AWS root CA might be necessary.

In this article, I have tried outlining to my best effort how a company can manage their CDN infrastructure in Akamai through infrastructure as code in a modern way - using this blog itself as a real-life example.

In DNB, we still have a lot of infrastructure that is managed in a more manual way, but we aspire to be a modern software company!

Part of this is sharing to the world what we do and how we do it, so I hope you have enjoyed a little peak in to the secret sauce of CDN configuration, the DNB way!

Disclaimer: The views and opinions expressed in this article are those of the author and do not necessarily reflect the official policy or position of DNB.

© DNB

To dnb.no

Informasjonskapsler

DNB samler inn og analyserer data om din brukeratferd på våre nettsider.