Dapr and Mastodon

Published: Jan 25, 2023 by Isaac Johnson

We last explored Dapr Secrets nearly two years ago. I thought it might be nice to circle back on them and see how we can use Dapr.io with Azure KeyVault (AKV) and GCP Secrets Manager. My hope was then to tie them into API Posts to Mastodon.

I ended up getting delayed a day because my whole system started to go whacky. I had 15m docker pulls, networking was dropping out. Additionally, at home, my Wi-Fi was getting spottier by the day. The whole collective had not had a restart in 40days and it showed. When I restarted, key servers got new IPs; like the database for Harbor, which then blocked other pods, the Kubernetes Master so the Nodes couldn’t rejoin. It was a mess. But after lots of poking about, things are happy again - but delayed this post.

Dapr and Secrets

First, let’s look at how we check our Dapr version and upgrade, if needed.

First, we can check our Dapr version

$ dapr version
CLI version: 1.8.0
Runtime version: n/a

Then we can upgrade if needed (1.8.0 is pretty old at this point)

$ wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash
Getting the latest Dapr CLI...
Your system is linux_amd64

Dapr CLI is detected:
CLI version: 1.8.0
Runtime version: n/a
Reinstalling Dapr CLI - /usr/local/bin/dapr...

Installing v1.9.1 Dapr CLI...
Downloading https://github.com/dapr/cli/releases/download/v1.9.1/dapr_linux_amd64.tar.gz ...
[sudo] password for builder:
Sorry, try again.
[sudo] password for builder:
dapr installed into /usr/local/bin successfully.
CLI version: 1.9.1
Runtime version: n/a

To get started with Dapr, please visit https://docs.dapr.io/getting-started/

We can see the CLI is running locally

$ dapr version
CLI version: 1.9.1
Runtime version: n/a

Next, we bring our Kubernetes cluster up to the same version

I can try and install

$ dapr init -k
⌛  Making the jump to hyperspace...
ℹ️  Note: To install Dapr using Helm, see here: https://docs.dapr.io/getting-started/install-dapr-kubernetes/#install-with-helm-advanced

ℹ️  Container images will be pulled from Docker Hub
❌  Deploying the Dapr control plane to your cluster...
❌  cannot re-use a name that is still in use

Verify it’s running in k8s

$ kubectl get pods --namespace dapr-system
NAME                                    READY   STATUS    RESTARTS          AGE
dapr-sentry-7f8db4ff47-h98l8            1/1     Running   0                 11d
dapr-placement-server-0                 1/1     Running   0                 11d
dapr-dashboard-6b868c46fb-gxzrq         1/1     Running   0                 11d
dapr-sidecar-injector-75948876c-g5hrz   1/1     Running   0                 11d
dapr-operator-6bd7f7bfdd-66f6w          1/1     Running   336 (2d14h ago)   11d

You can check which version is running by port-forwarding to the dashboard

builder@DESKTOP-72D2D9T:~/Workspaces/jekyll-blog$ kubectl port-forward dapr-dashboard-6b868c46fb-gxzrq -n dapr-system 80
80:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Handling connection for 8080

/content/images/2023/01/mastodon-02.png

Azure Key Vault

Before we go farther, let’s go into Azure and create a key vault.

We could use the UI

/content/images/2023/01/mastodon-03.png

But I’ll just quickly use the command line:

$ az group create --name daprkvrg --location centralus
{
  "id": "/subscriptions/d955c0ba-eeee-eeee-eeee-eeeeeeff/resourceGroups/daprkvrg",
  "location": "centralus",
  "managedBy": null,
  "name": "daprkvrg",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": null,
  "type": "Microsoft.Resources/resourceGroups"
}
$ az keyvault create --location centralus --name daprakv -g daprkvrg
{
  "id": "/subscriptions/d955c0ba-eeee-eeee-eeee-eeeeeeff/resourceGroups/daprkvrg/providers/Microsoft.KeyVault/vaults/daprakv",
  "location": "centralus",
  "name": "daprakv",
  "properties": {
    "accessPolicies": [
      {
        "applicationId": null,
        "objectId": "1f5d835c-b129-41e6-b2fe-5858a5f4e41a",
        "permissions": {
          "certificates": [
            "all"
          ],
          "keys": [
            "all"
          ],
          "secrets": [
            "all"
          ],
          "storage": [
            "all"
          ]
        },
        "tenantId": "28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a"
      }
    ],
    "createMode": null,
    "enablePurgeProtection": null,
    "enableRbacAuthorization": null,
    "enableSoftDelete": true,
    "enabledForDeployment": false,
    "enabledForDiskEncryption": null,
    "enabledForTemplateDeployment": null,
    "hsmPoolResourceId": null,
    "networkAcls": null,
    "privateEndpointConnections": null,
    "provisioningState": "Succeeded",
    "publicNetworkAccess": "Enabled",
    "sku": {
      "family": "A",
      "name": "standard"
    },
    "softDeleteRetentionInDays": 90,
    "tenantId": "28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a",
    "vaultUri": "https://daprakv.vault.azure.net/"
  },
  "resourceGroup": "daprkvrg",
  "systemData": {
    "createdAt": "2023-01-23T12:47:59.504000+00:00",
    "createdBy": "isaac.johnson@gmail.com",
    "createdByType": "User",
    "lastModifiedAt": "2023-01-23T12:47:59.504000+00:00",
    "lastModifiedBy": "isaac.johnson@gmail.com",
    "lastModifiedByType": "User"
  },
  "tags": {},
  "type": "Microsoft.KeyVault/vaults"
}

To add AKV to Dapr, we’ll need to create an SP we can tie to an Azure Key Vault:

$ az login
$ az ad sp create-for-rbac --name tpkdapr.isaacjohnsongmail.onmicrosoft.com
{
  "appId": "61581835-asdf-asdf-asdf-asdfasdfadsf",
  "displayName": "tpkdapr.isaacjohnsongmail.onmicrosoft.com",
  "password": "1asdfasdfasdfasdfasfasdasdfasdfah",
  "tenant": "28c575f6-asdf-asdf-asdf-asdfasdfasfasdf"
}

Next, I’ll need to grant this SP access to the AKV.. This should work

$ az role assignment create --role "Key Vault Secrets Officer" --assignee 61581835-asdf-asdf-asdf-asdfasdfadsf --scope /subscriptions/d955c0ba-eeee-eeee-eeee-eeeeeeff/resourceGroups/daprkvrg/providers/Microsoft.KeyVault/vaults/daprakv
{
  "canDelegate": null,
  "condition": null,
  "conditionVersion": null,
  "description": null,
  "id": "/subscriptions/d955c0ba-eeee-eeee-eeee-eeeeeeff/resourceGroups/daprkvrg/providers/Microsoft.KeyVault/vaults/daprakv/providers/Microsoft.Authorization/roleAssignments/46d59609-3a9d-48ff-88bc-25d4c57f3b17",
  "name": "46d59609-3a9d-48ff-88bc-25d4c57f3b17",
  "principalId": "fa102a38-aaaa-aaaa-aaaa-aaaaa01157b5",
  "principalType": "ServicePrincipal",
  "resourceGroup": "daprkvrg",
  "roleDefinitionId": "/subscriptions/d955c0ba-eeee-eeee-eeee-eeeeeeff/providers/Microsoft.Authorization/roleDefinitions/b86a8fe4-44ce-4948-aee5-eccb2c155cd7",
  "scope": "/subscriptions/d955c0ba-eeee-eeee-eeee-eeeeeeff/resourceGroups/daprkvrg/providers/Microsoft.KeyVault/vaults/daprakv",
  "type": "Microsoft.Authorization/roleAssignments"
}

But I noticed the “principalID” wasn’t the AppID I expected. I went back into the Access Policies and did it again manually and then saw my application list properly

/content/images/2023/01/mastodon-04.png

Setting up Dapr

In Kubernetes, I’ll want to set the clientsecret (password) to a Kubernetes secret

$ kubectl create secret generic daprakvkey --from-literal=password=1asdfasdfasdfasdfasfasdasdfasdfah
secret/daprakvkey created

You can do a sanity check that there were no funny characters and that it exists by pulling the secret back and decoding:

$ kubectl get secret daprakvkey -o json | jq -r .data.password | base64
--decode
1asdfasdfasdfasdfasfasdasdfasdfah

We can now use it in Dapr

$ cat azurekeyvault.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: azurekeyvault
spec:
  type: secretstores.azure.keyvault
  version: v1
  metadata:
  - name: vaultName
    value: "daprakv"
  - name: azureTenantId
    value: "28c575f6-aaaa-aaaa-aaaa-aaaaaa0eb4a"
  - name: azureClientId
    value: "61581835-eeee-eeee-eeee-eeeeeb1d583"
  - name: azureClientSecret
    secretKeyRef:
      name: "daprakvkey"
      key: "password"
auth:
  secretStore: kubernetes

which we can then apply

$ kubectl apply -f azurekeyvault.yaml
component.dapr.io/azurekeyvault created

To do a quick test, I’ll create a secret in AKV

$ az keyvault secret set --vault-name daprakv --name mytestsecret --value hellofromakv
{
  "attributes": {
    "created": "2023-01-23T13:05:41+00:00",
    "enabled": true,
    "expires": null,
    "notBefore": null,
    "recoveryLevel": "Recoverable+Purgeable",
    "updated": "2023-01-23T13:05:41+00:00"
  },
  "contentType": null,
  "id": "https://daprakv.vault.azure.net/secrets/mytestsecret/a240d11b435f4b74a667dd666329cf1f",
  "kid": null,
  "managed": null,
  "name": "mytestsecret",
  "tags": {
    "file-encoding": "utf-8"
  },
  "value": "hellofromakv"
}

Then I’ll rotate a Dapr instrumented pod in my cluster

builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl delete pod addapp-677b754f58-scw6k
pod "addapp-677b754f58-scw6k" deleted
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep add
addapp-677b754f58-59m8m                                  0/2     ContainerCreating   0                  42s

My pulls are taking upwards of 10m. This, i discovered, was due to Docker rate limiting (recall in my last post why I moved to HarborCR)

builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl delete pod addapp-677b754f58-scw6k
pod "addapp-677b754f58-scw6k" deleted
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep add
addapp-677b754f58-59m8m                                  0/2     ContainerCreating   0                  42s
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep add
addapp-677b754f58-59m8m                                  0/2     ContainerCreating   0                  54s
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep add
addapp-677b754f58-59m8m                                  0/2     ContainerCreating   0                  57s
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep add
addapp-677b754f58-59m8m                                  0/2     ContainerCreating   0                  66s
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep add
addapp-677b754f58-59m8m                                  0/2     ContainerCreating   0                  2m7s
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep add
addapp-677b754f58-59m8m                                  0/2     ContainerCreating   0                  4m20s
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep add
addapp-677b754f58-59m8m                                  0/2     ContainerCreating   0                  4m42s
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl describe pod addapp-677b754f58-59m8m | tail -n5
Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Scheduled  4m56s  default-scheduler  Successfully assigned default/addapp-677b754f58-59m8m to hp-hp-elitebook-850-g2
  Normal  Pulling    4m54s  kubelet            Pulling image "idjohnson/coboladder:0.0.3"


# Checking if we have a rate limit

builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  4414    0  4414    0     0  22070      0 --:--:-- --:--:-- --:--:-- 22070
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ curl --head -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest
HTTP/1.1 200 OK
content-length: 2782
content-type: application/vnd.docker.distribution.manifest.v1+prettyjws
docker-content-digest: sha256:767a3815c34823b355bed31760d5fa3daca0aec2ce15b217c9cd83229e0e2020
docker-distribution-api-version: registry/2.0
etag: "sha256:767a3815c34823b355bed31760d5fa3daca0aec2ce15b217c9cd83229e0e2020"
date: Mon, 23 Jan 2023 13:24:52 GMT
strict-transport-security: max-age=31536000
ratelimit-limit: 100;w=21600
ratelimit-remaining: 97;w=21600
docker-ratelimit-source: 73.242.50.46

It took a fair amount of time, but eventually my container came up.

One gotcha I had was the dogs all showed commands using localhost:3601 (e.g. Secrets How-To)

[root@addapp-677b754f58-4gppg cobol]# curl http://127.0.0.1:3601/v1.0/secrets/azurekeyvault/mytestsecret
curl: (7) Failed to connect to 127.0.0.1 port 3601: Connection refused

However, it really should be localhost:$DAPR_HTTP_PORT. In our setup, we can see DAPR is serving up on port 3500:

[root@addapp-677b754f58-4gppg cobol]# export | grep DAPR
declare -x DAPR_GRPC_PORT="50001"
declare -x DAPR_HTTP_PORT="3500"

Once I realized that, it was easy to fetch a Dapr secret

[root@addapp-677b754f58-4gppg cobol]# curl http://localhost:$DAPR_HTTP_PORT/v1.0/secrets/azurekeyvault/mytestsecret && echo
{"mytestsecret":"hellofromakv"}
[root@addapp-677b754f58-4gppg cobol]# curl http://127.0.0.1:3500/v1.0/secrets/azurekeyvault/mytestsecret
{"mytestsecret":"hellofromakv"}

There is nothing that suggests Dapr can update secrets, but if it could, I would suspect it would be wrapped in a POST.

Just to experiment, I tried a few possibilities (and as expected, they did not work)

[root@addapp-677b754f58-4gppg cobol]# curl -X POST http://localhost:3500/v1.0/secrets/azurekeyvault/mytestsecret -H "Content-Type: application/json" -d '{"mytestsecret":"anewvalue"}'
{"errorCode":"ERR_DIRECT_INVOKE","message":"failed getting app id either from the URL path or the header dapr-app-id"}

[root@addapp-677b754f58-4gppg cobol]# curl -X POST http://localhost:3500/v1.0/secrets/azurekeyvault -H "Content-Type: application/json" -d '{"mytestsecret":"anewvalue"}'
{"errorCode":"ERR_DIRECT_INVOKE","message":"failed getting app id either from the URL path or the header dapr-app-id"}

[root@addapp-677b754f58-4gppg cobol]# curl -X POST http://localhost:3500/v1.0/secrets/azurekeyvault/mytestsecret -H "Content-Type: application/json" -d '{"value":"anewvalue"}'
{"errorCode":"ERR_DIRECT_INVOKE","message":"failed getting app id either from the URL path or the header dapr-app-id"}

[root@addapp-677b754f58-4gppg cobol]# curl -X POST http://localhost:3500/v1.0/secrets/azurekeyvault/mytestsecret/anewvalue -H "Content-Type: application/json"
{"errorCode":"ERR_DIRECT_INVOKE","message":"failed getting app id either from the URL path or the header dapr-app-id"}

GCP Secret Store

Nowadays I do as much with GCP as I do Azure.

We should give the GCP Secret Manager a look as well.

I’ll go to a GCP Project and create a new Service Account for this work:

/content/images/2023/01/mastodon-05.png

I’ll set some details (I’ll likely use this same credential later for Pub/Sub and more)

/content/images/2023/01/mastodon-06.png

Since I plan to use this for a few services, I’ll add Storage Admin and Pub/Sub Editor roles to Secret Manager Secret Accessor.

/content/images/2023/01/mastodon-07.png

You can leave the last section blank and click “Done” to create

/content/images/2023/01/mastodon-08.png

Next, use the 3-dot menu to “Manage Keys”

/content/images/2023/01/mastodon-09.png

Click “Add Key” and select “Create new key”

/content/images/2023/01/mastodon-10.png

Choose JSON and click “Create”

/content/images/2023/01/mastodon-11.png

That should download locally and we’ll use to fill in the blanks

$ cat /mnt/c/Users/isaac/Downloads/myanthosproject2-b8b0858893d5.json
{
  "type": "service_account",
  "project_id": "myanthosproject2",
  "private_key_id": "b8b0858893d512341234123412341234f3e4",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgasasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfadfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfas8g==\n-----END PRIVATE KEY-----\n",
  "client_email": "daprservice2@myanthosproject2.iam.gserviceaccount.com",
  "client_id": "103334445556697",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/daprservice2%40myanthosproject2.iam.gserviceaccount.com"
}

That will then translate to the below:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: gcpsecretmanager
spec:
  type: secretstores.gcp.secretmanager
  version: v1
  metadata:
  - name: type
    value: "service_account"
  - name: project_id
    value: "myanthosproject2"
  - name: private_key_id
    value: "b8b0858893d512341234123412341234f3e4"
  - name: client_email
    value: "daprservice2@myanthosproject2.iam.gserviceaccount.com"
  - name: client_id
    value: "103334445556697"
  - name: auth_uri
    value: "https://accounts.google.com/o/oauth2/auth"
  - name: token_uri
    value: "https://oauth2.googleapis.com/token"
  - name: auth_provider_x509_cert_url
    value: "https://www.googleapis.com/oauth2/v1/certs"
  - name: client_x509_cert_url
    value: "https://www.googleapis.com/robot/v1/metadata/x509/daprservice2%40myanthosproject2.iam.gserviceaccount.com"
  - name: private_key
    value: "-----BEGIN PRIVATE KEY-----\nnMIIEvAIBADANBgasasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfadfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfas8g\n-----END PRIVATE KEY-----\n"

I’ll then apply it

$ kubectl apply -f gcpsecretmanager.yaml
component.dapr.io/gcpsecretmanager created

And rotate a pod

$ kubectl delete pod addapp-677b754f58-4gppg
pod "addapp-677b754f58-4gppg" deleted

While that rotates, I’ll circle back to GCP and make a secret

First, I’ll need to enable the Secret Manager API as I haven’t in the past

/content/images/2023/01/mastodon-12.png

We can see the price is free up to 10k uses a month

/content/images/2023/01/mastodon-13.png

Next, I’ll create a secret

/content/images/2023/01/mastodon-14.png

I’ll give it a name and a value, then click “Create Secret”

/content/images/2023/01/mastodon-15.png

We can now see it in Secret Manager. If we click the dots menu, we can “View secret value”

/content/images/2023/01/mastodon-16.png

and confirm it is indeed enabled and matches what we typed earlier

/content/images/2023/01/mastodon-17.png

I have to admit, these long container pulls are driving me batty.

$ kubectl get pods | grep add
addapp-677b754f58-4sq2v                                  0/2     ContainerCreating   0                  6m33s

I’m going to update the Deployment to pull the image only when needed

$ kubectl get deployments addapp -o yaml > addapp.yaml
$ kubectl get deployments addapp -o yaml > addapp.yaml.bak
$ vi addapp.yaml
$ diff addapp.yaml addapp.yaml.bak
44c44
<         imagePullPolicy: IfNotPresent
---
>         imagePullPolicy: Always
$ kubectl apply -f ./addapp.yaml
deployment.apps/addapp configured

I did it to myself, so I’ll have to wait this first time, but it should go better the next time

$ kubectl describe pod addapp-549c9cbfdd-h9fs4 | tail -n4
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  47s   default-scheduler  Successfully assigned default/addapp-549c9cbfdd-h9fs4 to hp-hp-elitebook-850-g2
  Normal  Pulling    45s   kubelet            Pulling image "idjohnson/coboladder:0.0.3"

$ kubectl describe pod addapp-549c9cbfdd-h9fs4 | tail -n4
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  15m   default-scheduler  Successfully assigned default/addapp-549c9cbfdd-h9fs4 to hp-hp-elitebook-850-g2
  Normal  Pulling    15m   kubelet            Pulling image "idjohnson/coboladder:0.0.3"

$ kubectl describe pod addapp-549c9cbfdd-h9fs4 | tail -n4
  Normal  Started    20m   kubelet            Started container add
  Normal  Pulled     20m   kubelet            Container image "docker.io/daprio/daprd:1.9.5" already present on machine
  Normal  Created    20m   kubelet            Created container daprd
  Normal  Started    20m   kubelet            Started container daprd

We can now exec into the pod

$ kubectl exec -it addapp-549c9cbfdd-h9fs4 -- /bin/bash
Defaulted container "add" out of: add, daprd
[root@addapp-549c9cbfdd-h9fs4 cobol]#

And verify we can pull the secret

[root@addapp-549c9cbfdd-h9fs4 cobol]# curl http://localhost:$DAPR_HTTP_PORT/v1.0/secrets/gcpsecretmanager/mytestsecret && echo
{"mytestsecret":"Some Value For Dapr to See"}

Additionally, we haven’t switched secrets providers, we merely added one. So I can still fetch secrets from AKV on the same pod

[root@addapp-549c9cbfdd-h9fs4 cobol]# curl http://localhost:$DAPR_HTTP_PORT/v1.0/secrets/azurekeyvault/mytestsecret && echo
{"mytestsecret":"hellofromakv"}

Mastodon

The goal of all this was to post to Mastodon. There are a lot of good Mastodon servers out there. I use Noc.social myself, but there are many good options.

Be aware, Mastodon is pretty hot right now, so the UI may change. Today, we go to Preferences on the right-hand side

/content/images/2023/01/mastodon-18.png

Then go to Development

/content/images/2023/01/mastodon-19.png

Here, we’ll make a “New application”

/content/images/2023/01/mastodon-20.png

I’ll give it a name

/content/images/2023/01/mastodon-21.png

I left the default scopes (read, write, follow) but also added “crypto”. It might not be neccessary to add crypto, but i like the idea i could use E2E encryption.

/content/images/2023/01/mastodon-22.png

This should create an Application

/content/images/2023/01/mastodon-23.png

And from there, I can see my Client ID, Secret and Token

/content/images/2023/01/mastodon-24.png

I should note, you can follow the more standard OAuth2 flow and use the Client ID and Secret paired with your Username and password to get the access token dynamically: https://github.com/felx/mastodon-documentation/blob/master/Using-the-API/Testing-with-cURL.md

But for our purposes, I’ll satisfied with just using the access token provided (and rotatable) through the Developer UI.

We can now do a test post

$ curl -X POST -H 'Authorization: Bearer cl5EnOtaRealTokenButSimilarToThisQdGo' -H 'Content-Type: application/json' -d '{"status":"Te
st Post, please ignore"}'  https://noc.social/api/v1/statuses
{"id":"109744259200323382","created_at":"2023-01-24T12:48:21.963Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"https://noc.social/users/Ijohnson/statuses/109744259200323382","url":"https://noc.social/@Ijohnson/109744259200323382","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eTest Post, please ignore\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"TPKGithubPosting","website":"https://github.com/idjohnson"},"account":{"id":"109299867199050307","username":"Ijohnson","acct":"Ijohnson","display_name":"Ijohnson","locked":false,"bot":false,"discoverable":null,"group":false,"created_at":"2022-11-07T00:00:00.000Z","note":"","url":"https://noc.social/@Ijohnson","avatar":"https://noc.social/system/accounts/avatars/109/299/867/199/050/307/original/cc07f8a94f1ffadf.jpeg","avatar_static":"https://noc.social/system/accounts/avatars/109/299/867/199/050/307/original/cc07f8a94f1ffadf.jpeg","header":"https://noc.social/headers/original/missing.png","header_static":"https://noc.social/headers/original/missing.png","followers_count":4,"following_count":6,"statuses_count":31,"last_status_at":"2023-01-24","noindex":false,"emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}

And I can see it posted

/content/images/2023/01/mastodon-25.png

I can then delete it manually

/content/images/2023/01/mastodon-26.png

We can fetch an old status

$ curl -X GET -H 'Authorization: Bearer cl5EnOtaRealTokenButSimilarToThisQdGo' -H 'Content-Type: application/json' https://noc.social/api/v1/statuses/109706528335457979
{"id":"109706528335457979","created_at":"2023-01-17T20:52:54.652Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"https://noc.social/users/Ijohnson/statuses/109706528335457979","url":"https://noc.social/@Ijohnson/109706528335457979","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eToday I wrapped a 2 part series on \u003ca href=\"https://noc.social/tags/IBM\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eIBM\u003c/span\u003e\u003c/a\u003e \u003ca href=\"https://noc.social/tags/Instana\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eInstana\u003c/span\u003e\u003c/a\u003e. Part 1 covers Setup, \u003ca href=\"https://noc.social/tags/kubernetes\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003ekubernetes\u003c/span\u003e\u003c/a\u003e Website monitoring and \u003ca href=\"https://noc.social/tags/OpenTelemetry\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eOpenTelemetry\u003c/span\u003e\u003c/a\u003e tracing. Today I posted part 2 which covers \u003ca href=\"https://noc.social/tags/gcp\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003egcp\u003c/span\u003e\u003c/a\u003e \u003ca href=\"https://noc.social/tags/databases\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003edatabases\u003c/span\u003e\u003c/a\u003e \u003ca href=\"https://noc.social/tags/alerting\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003ealerting\u003c/span\u003e\u003c/a\u003e via \u003ca href=\"https://noc.social/tags/pagerduty\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003epagerduty\u003c/span\u003e\u003c/a\u003e and log integration with \u003ca href=\"https://noc.social/tags/mezmo\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003emezmo\u003c/span\u003e\u003c/a\u003e / \u003ca href=\"https://noc.social/tags/logdna\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003elogdna\u003c/span\u003e\u003c/a\u003e and \u003ca href=\"https://noc.social/tags/coralogix\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003ecoralogix\u003c/span\u003e\u003c/a\u003e. \u003c/p\u003e\u003cp\u003ePart 1: \u003ca href=\"https://freshbrewed.science/2023/01/10/Instana-Part1.html\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"ellipsis\"\u003efreshbrewed.science/2023/01/10\u003c/span\u003e\u003cspan class=\"invisible\"\u003e/Instana-Part1.html\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e\u003cp\u003ePart 2: \u003ca href=\"https://freshbrewed.science/2023/01/17/Instana-Part2.html\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"ellipsis\"\u003efreshbrewed.science/2023/01/17\u003c/span\u003e\u003cspan class=\"invisible\"\u003e/Instana-Part2.html\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Web","website":null},"account":{"id":"109299867199050307","username":"Ijohnson","acct":"Ijohnson","display_name":"Ijohnson","locked":false,"bot":false,"discoverable":null,"group":false,"created_at":"2022-11-07T00:00:00.000Z","note":"","url":"https://noc.social/@Ijohnson","avatar":"https://noc.social/system/accounts/avatars/109/299/867/199/050/307/original/cc07f8a94f1ffadf.jpeg","avatar_static":"https://noc.social/system/accounts/avatars/109/299/867/199/050/307/original/cc07f8a94f1ffadf.jpeg","header":"https://noc.social/headers/original/missing.png","header_static":"https://noc.social/headers/original/missing.png","followers_count":4,"following_count":6,"statuses_count":30,"last_status_at":"2023-01-24","noindex":false,"emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[{"name":"Coralogix","url":"https://noc.social/tags/Coralogix"},{"name":"logdna","url":"https://noc.social/tags/logdna"},{"name":"mezmo","url":"https://noc.social/tags/mezmo"},{"name":"pagerduty","url":"https://noc.social/tags/pagerduty"},{"name":"alerting","url":"https://noc.social/tags/alerting"},{"name":"databases","url":"https://noc.social/tags/databases"},{"name":"gcp","url":"https://noc.social/tags/gcp"},{"name":"opentelemetry","url":"https://noc.social/tags/opentelemetry"},{"name":"kubernetes","url":"https://noc.social/tags/kubernetes"},{"name":"instana","url":"https://noc.social/tags/instana"},{"name":"ibm","url":"https://noc.social/tags/ibm"}],"emojis":[],"card":{"url":"https://freshbrewed.science/2023/01/10/Instana-Part1.html","title":"Instana: Part 1: Setup, K8s, Website and Open Telemetry","description":"Today we will dig into creating an Instana account and setting up Host and Kubernetes monitors. We’ll look into serverless (AWS Lambdas) and tracing through Open Telemetry.","type":"link","author_name":"","author_url":"","provider_name":"Fresh/Brewed","provider_url":"","html":"","width":0,"height":0,"image":null,"embed_url":"","blurhash":null},"poll":null}b

Which shows the guts of the status (content) to be Unicode HTML

\u003cp\u003eToday I wrapped a 2 part series on \u003ca href=\"https://noc.social/tags/IBM\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eIBM\u003c/span\u003e\u003c/a\u003e \u003ca href=\"https://noc.social/tags/Instana\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eInstana\u003c/span\u003e\u003c/a\u003e. Part 1 covers Setup, \u003ca href=\"https://noc.social/tags/kubernetes\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003ekubernetes\u003c/span\u003e\u003c/a\u003e Website monitoring and \u003ca href=\"https://noc.social/tags/OpenTelemetry\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eOpenTelemetry\u003c/span\u003e\u003c/a\u003e tracing. Today I posted part 2 which covers \u003ca href=\"https://noc.social/tags/gcp\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003egcp\u003c/span\u003e\u003c/a\u003e \u003ca href=\"https://noc.social/tags/databases\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003edatabases\u003c/span\u003e\u003c/a\u003e \u003ca href=\"https://noc.social/tags/alerting\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003ealerting\u003c/span\u003e\u003c/a\u003e via \u003ca href=\"https://noc.social/tags/pagerduty\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003epagerduty\u003c/span\u003e\u003c/a\u003e and log integration with \u003ca href=\"https://noc.social/tags/mezmo\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003emezmo\u003c/span\u003e\u003c/a\u003e / \u003ca href=\"https://noc.social/tags/logdna\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003elogdna\u003c/span\u003e\u003c/a\u003e and \u003ca href=\"https://noc.social/tags/coralogix\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003ecoralogix\u003c/span\u003e\u003c/a\u003e. \u003c/p\u003e\u003cp\u003ePart 1: \u003ca href=\"https://freshbrewed.science/2023/01/10/Instana-Part1.html\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"ellipsis\"\u003efreshbrewed.science/2023/01/10\u003c/span\u003e\u003cspan class=\"invisible\"\u003e/Instana-Part1.html\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e\u003cp\u003ePart 2: \u003ca href=\"https://freshbrewed.science/2023/01/17/Instana-Part2.html\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"ellipsis\"\u003efreshbrewed.science/2023/01/17\u003c/span\u003e\u003cspan class=\"invisible\"\u003e/Instana-Part2.html\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e

I can use a quick online converter to see the raw HTML

/content/images/2023/01/mastodon-27.png

I’m curious if my “#” entries will be automatically turned into internal labels or i have to do it manaully. Let’s test that.

$ curl -X POST -H 'Authorization: Bearer cl5EnOtaRealTokenButSimilarToThisQdGo' -H 'Content-Type: application/json' -d '{"status":"Test 2, #testing <a href=\"http://freshbrewed.science\">blog</a>"}'  https://noc.social/api/v1/statuses
{"id":"109744295782598916","created_at":"2023-01-24T12:57:40.161Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"https://noc.social/users/Ijohnson/statuses/109744295782598916","url":"https://noc.social/@Ijohnson/109744295782598916","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eTest 2, \u003ca href=\"https://noc.social/tags/testing\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003etesting\u003c/span\u003e\u003c/a\u003e \u0026lt;a href=\u0026quot;\u003ca href=\"http://freshbrewed.science\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttp://\u003c/span\u003e\u003cspan class=\"\"\u003efreshbrewed.science\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e\u0026quot;\u0026gt;blog\u0026lt;/a\u0026gt;\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"TPKGithubPosting","website":"https://github.com/idjohnson"},"account":{"id":"109299867199050307","username":"Ijohnson","acct":"Ijohnson","display_name":"Ijohnson","locked":false,"bot":false,"discoverable":null,"group":false,"created_at":"2022-11-07T00:00:00.000Z","note":"","url":"https://noc.social/@Ijohnson","avatar":"https://noc.social/system/accounts/avatars/109/299/867/199/050/307/original/cc07f8a94f1ffadf.jpeg","avatar_static":"https://noc.social/system/accounts/avatars/109/299/867/199/050/307/original/cc07f8a94f1ffadf.jpeg","header":"https://noc.social/headers/original/missing.png","header_static":"https://noc.social/headers/original/missing.png","followers_count":4,"following_count":6,"statuses_count":31,"last_status_at":"2023-01-24","noindex":false,"emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[{"name":"testing","url":"https://noc.social/tags/testing"}],"emojis":[],"card":null,"poll":null}

Interestingly the # tag works just fine, but the URL was a fail

/content/images/2023/01/mastodon-28.png

My final test is to pack in Unicode with a URL

$ curl -X POST -H 'Authorization: Bearer cl5EnOtaRealTokenButSimilarToThisQdGo' -H 'Content-Type: application/json' -d '{"status":"Test 2, #testing https://freshbrewed.science \u003ca href=\"https://freshbrewed.science/2023/01/10/Instana-Part1.html\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003eSome Post\u003c/a\u003e"}'  https://noc.social/api/v1/statuses
{"id":"109744325594361896","created_at":"2023-01-24T13:05:15.058Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"https://noc.social/users/Ijohnson/statuses/109744325594361896","url":"https://noc.social/@Ijohnson/109744325594361896","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eTest 2, \u003ca href=\"https://noc.social/tags/testing\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003etesting\u003c/span\u003e\u003c/a\u003e \u003ca href=\"https://freshbrewed.science\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003efreshbrewed.science\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e \u0026lt;a href=\u0026quot;\u003ca href=\"https://freshbrewed.science/2023/01/10/Instana-Part1.html\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"ellipsis\"\u003efreshbrewed.science/2023/01/10\u003c/span\u003e\u003cspan class=\"invisible\"\u003e/Instana-Part1.html\u003c/span\u003e\u003c/a\u003e\u0026quot; target=\u0026quot;_blank\u0026quot; rel=\u0026quot;nofollow noopener noreferrer\u0026quot;\u0026gt;Some Post\u0026lt;/a\u0026gt;\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"TPKGithubPosting","website":"https://github.com/idjohnson"},"account":{"id":"109299867199050307","username":"Ijohnson","acct":"Ijohnson","display_name":"Ijohnson","locked":false,"bot":false,"discoverable":null,"group":false,"created_at":"2022-11-07T00:00:00.000Z","note":"","url":"https://noc.social/@Ijohnson","avatar":"https://noc.social/system/accounts/avatars/109/299/867/199/050/307/original/cc07f8a94f1ffadf.jpeg","avatar_static":"https://noc.social/system/accounts/avatars/109/299/867/199/050/307/original/cc07f8a94f1ffadf.jpeg","header":"https://noc.social/headers/original/missing.png","header_static":"https://noc.social/headers/original/missing.png","followers_count":4,"following_count":6,"statuses_count":31,"last_status_at":"2023-01-24","noindex":false,"emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[{"name":"testing","url":"https://noc.social/tags/testing"}],"emojis":[],"card":null,"poll":null}

While the URL worked, the packed Unicode did not

/content/images/2023/01/mastodon-29.png

Mastodon and Dapr Secrets

Now that we have a nice secret store as provided via Dapr, could I use that in my Summerwind Github Action Runners?

Note: This ultimately did not work. I think it has to do with how the Summerwind injects DnD containers. It was in the middle of testing this my whole system pooped out and delayed me. But I’ll show you my work regardless. I ended up just using a GH Actions Secret instead

First, we need to realize that the Github Action Runner isn’t a standard deployment, rather a “RunnerDeployment” as setup via actions.summerwind.dev

$ kubectl get RunnerDeployment new-jekyllrunner-deployment -o yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"actions.summerwind.dev/v1alpha1","kind":"RunnerDeployment","metadata":{"annotations":{},"name":"new-jekyllrunner-deployment","namespace":"default"},"spec":{"replicas":2,"selector":null,"template":{"metadata":{},"spec":{"dockerEnabled":true,"dockerdContainerResources":{},"env":[{"name":"AWS_DEFAULT_REGION","value":"us-east-1"},{"name":"AWS_ACCESS_KEY_ID","valueFrom":{"secretKeyRef":{"key":"USER_NAME","name":"awsjekyll"}}},{"name":"AWS_SECRET_ACCESS_KEY","valueFrom":{"secretKeyRef":{"key":"PASSWORD","name":"awsjekyll"}}},{"name":"DATADOG_API_KEY","valueFrom":{"secretKeyRef":{"key":"DDAPIKEY","name":"ddjekyll"}}}],"image":"harbor.freshbrewed.science/freshbrewedprivate/myghrunner:1.1.13","imagePullPolicy":"IfNotPresent","imagePullSecrets":[{"name":"myharborreg"}],"labels":["new-jekyllrunner-deployment"],"repository":"idjohnson/jekyll-blog","resources":{}}}}}
  creationTimestamp: "2022-08-31T12:21:29Z"
  generation: 1
  name: new-jekyllrunner-deployment
  namespace: default
  resourceVersion: "72454949"
  uid: 05168427-0f0b-4cee-a692-904ae409ed1b
spec:
  effectiveTime: null
  replicas: 2
  selector: null
  template:
    metadata: {}
    spec:
      dockerEnabled: true
      dockerdContainerResources: {}
      env:
      - name: AWS_DEFAULT_REGION
        value: us-east-1
      - name: AWS_ACCESS_KEY_ID
        valueFrom:
          secretKeyRef:
            key: USER_NAME
            name: awsjekyll
      - name: AWS_SECRET_ACCESS_KEY
        valueFrom:
          secretKeyRef:
            key: PASSWORD
            name: awsjekyll
      - name: DATADOG_API_KEY
        valueFrom:
          secretKeyRef:
            key: DDAPIKEY
            name: ddjekyll
      image: harbor.freshbrewed.science/freshbrewedprivate/myghrunner:1.1.13
      imagePullPolicy: IfNotPresent
      imagePullSecrets:
      - name: myharborreg
      labels:
      - new-jekyllrunner-deployment
      repository: idjohnson/jekyll-blog
      resources: {}
status:
  availableReplicas: 2
  desiredReplicas: 2
  readyReplicas: 2
  replicas: 2
  updatedReplicas: 2

But I should be able to just add in the Dapr annotations to the spec.metadata to instrument it with Dapr.

builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get RunnerDeployment new-jekyllrunner-deployment -o yaml > myjekyllrunner.yaml
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get RunnerDeployment new-jekyllrunner-deployment -o yaml > myjekyllrunner.yaml.bak
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ vi myjekyllrunner.yaml

Then showing the changes:

builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ diff -C 10 myjekyllrunner.yaml myjekyllrunner.yaml.bak
*** myjekyllrunner.yaml 2023-01-24 07:17:05.246476950 -0600
--- myjekyllrunner.yaml.bak     2023-01-24 07:16:25.623693832 -0600
***************
*** 8,33 ****
    generation: 1
    name: new-jekyllrunner-deployment
    namespace: default
    resourceVersion: "72454949"
    uid: 05168427-0f0b-4cee-a692-904ae409ed1b
  spec:
    effectiveTime: null
    replicas: 2
    selector: null
    template:
!     metadata:
!       annotations:
!         dapr.io/app-id: jekyllrunner
!         dapr.io/app-port: "4000"
!         dapr.io/config: appconfig
!         dapr.io/enabled: "true"
      spec:
        dockerEnabled: true
        dockerdContainerResources: {}
        env:
        - name: AWS_DEFAULT_REGION
          value: us-east-1
        - name: AWS_ACCESS_KEY_ID
          valueFrom:
            secretKeyRef:
              key: USER_NAME
--- 8,28 ----
    generation: 1
    name: new-jekyllrunner-deployment
    namespace: default
    resourceVersion: "72454949"
    uid: 05168427-0f0b-4cee-a692-904ae409ed1b
  spec:
    effectiveTime: null
    replicas: 2
    selector: null
    template:
!     metadata: {}
      spec:
        dockerEnabled: true
        dockerdContainerResources: {}
        env:
        - name: AWS_DEFAULT_REGION
          value: us-east-1
        - name: AWS_ACCESS_KEY_ID
          valueFrom:
            secretKeyRef:
              key: USER_NAME

Applying

$ kubectl apply -f myjekyllrunner.yaml
runnerdeployment.actions.summerwind.dev/new-jekyllrunner-deployment configured

Though, this doesn’t look like it applied it at the pod level

builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep runner
new-jekyllrunner-deployment-4zzwr-5qx5w                  2/2     Running            0                  23s
new-jekyllrunner-deployment-4zzwr-jkjc6                  2/2     Running            0                  23s
new-jekyllrunner-deployment-tbq6z-k6xfb                  2/2     Terminating        0                  22h
new-jekyllrunner-deployment-tbq6z-sxjj5                  1/2     Terminating        0                  22h
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods | grep runner
new-jekyllrunner-deployment-4zzwr-5qx5w                  2/2     Running            0                  29s
new-jekyllrunner-deployment-4zzwr-jkjc6                  2/2     Running            0                  29s

Though I see the annotations

$ kubectl describe pod new-jekyllrunner-deployment-4zzwr-5qx5w
Name:         new-jekyllrunner-deployment-4zzwr-5qx5w
Namespace:    default
Priority:     0
Node:         hp-hp-elitebook-850-g2/192.168.1.57
Start Time:   Tue, 24 Jan 2023 07:18:02 -0600
Labels:       actions-runner=
              actions-runner-controller/inject-registration-token=true
              pod-template-hash=59fc694745
              runner-deployment-name=new-jekyllrunner-deployment
              runner-template-hash=5df47b8ccc
Annotations:  actions-runner-controller/token-expires-at: 2023-01-24T08:18:02-06:00
              dapr.io/app-id: jekyllrunner
              dapr.io/app-port: 4000
              dapr.io/config: appconfig
              dapr.io/enabled: true
              sync-time: 2023-01-24T13:18:01Z

However, clearly we can see it only has the ‘runner’ and ‘docker’ containers in the pod

builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl logs new-jekyllrunner-deployment-4zzwr-5qx5w
Defaulted container "runner" out of: runner, docker
2023-01-24 13:18:04.154  DEBUG --- Github endpoint URL https://github.com/
2023-01-24 13:18:07.913  DEBUG --- Passing --ephemeral to config.sh to enable the ephemeral runner.
2023-01-24 13:18:07.923  DEBUG --- Configuring the runner.

--------------------------------------------------------------------------------
|        ____ _ _   _   _       _          _        _   _                      |
|       / ___(_) |_| | | |_   _| |__      / \   ___| |_(_) ___  _ __  ___      |
|      | |  _| | __| |_| | | | | '_ \    / _ \ / __| __| |/ _ \| '_ \/ __|     |
|      | |_| | | |_|  _  | |_| | |_) |  / ___ \ (__| |_| | (_) | | | \__ \     |
|       \____|_|\__|_| |_|\__,_|_.__/  /_/   \_\___|\__|_|\___/|_| |_|___/     |
|                                                                              |
|                       Self-hosted runner registration                        |
|                                                                              |
--------------------------------------------------------------------------------

# Authentication


√ Connected to GitHub

# Runner Registration

I can see the annotations are there, but something about the Runner injection is likely blocking us.

We can sort this out with a quick pivot to using Github Actions secrets.

I’ll add my token as a secret for the Runner to consume

/content/images/2023/01/mastodon-30.png

I’ve been going back and forth between the right way to fetch the content for a post.

I could pull from a PR description using a Github Token

/content/images/2023/01/mastodon-31.png

      - name: Test GH first
        run: | 
            curl -s -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GHTOKEN" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/idjohnson/jekyll-blog/pulls/126 | jq -r .body
        env:
          GHTOKEN: $

Or using a new section in the post header

layout: post
title: "Dapr and Mostodon"
image: "/content/images/2023/01/mastodon-01.png"
date: '2023-01-19 01:01:31'
social: "In todays post we'll look at #Dapr secrets using #GCP Secret Manager and #Azure Keyvault. We'll then explore posting to #Mastodon using the REST API and #Github actions"

in an Action

      - name: Test GH first
        run: | 
            export LATESTFILE=`ls -l _posts/ | grep ".* 20[0-9][0-9]-[0-9][0-9].*markdown" | tail -n1 | sed 's/.* \(20[0-9][0-9]-[0-9][0-9].*markdown\)/\1/'`
            cat _posts/$LATESTFILE | grep "^social: " | head -n 1 | sed 's/^social: "\(.*\)"/\1/'

I decided to do the second approach as it doesn’t require me to format Github PR messages for Social posting.

Here we can see it pulled the value and posted

/content/images/2023/01/mastodon-31.png

which posted as such

/content/images/2023/01/mastodon-32.png

I added the URL to the JSON version in fact the following block posted twice

      - name: Test GH first
        run: | 
            export LATESTFILE=`ls -l _posts/ | grep ".* 20[0-9][0-9]-[0-9][0-9].*markdown" | tail -n1 | sed 's/.* \(20[0-9][0-9]-[0-9][0-9].*markdown\)/\1/'`
            export LATESTURL=`ls -l _posts/ | grep ".* 20[0-9][0-9]-[0-9][0-9].*markdown" | tail -n1 | sed 's/.* \(20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-\)\(.*\)\.markdown/\2.html/'`
            echo '{"status":"' | tr -d '\n' > payload.json
            cat _posts/$LATESTFILE | grep "^social: " | head -n 1 | sed 's/^social: "\(.*\)"/\1/' | tr -d '\n'>> payload.json
            cat _posts/$LATESTFILE | grep "^date: " | head -n 1 | sed "s/^date: .\([0-9]*\)-\([0-9]*\)-\([0-9]*\) .*/ https:\/\/freshbrewed.science\/\1\/\2\/\3\/$LATESTURL/g" | sed 's/.markdown/.html/' | tr -d '\n' >> payload.json
            echo '"}' >> payload.json

            curl -X POST -H "Authorization: Bearer $MASTODONAPI" -H 'Content-Type: application/json' -d @payload.json https://noc.social/api/v1/statuses

            export STATUSMSG=`cat _posts/$LATESTFILE | grep "^social: " | head -n 1 | sed 's/^social: "\(.*\)"/\1/'`
            curl --header "Authorization: Bearer $MASTODONAPI" --form "status=$STATUSMSG" "https://noc.social/api/v1/statuses"

        env:
          MASTODONAPI: $
          GHTOKEN: $

/content/images/2023/01/mastodon-33.png

At this point, I’m rather satisfied with my flow. I’ll prep (this post) to use it in the action when merged to main.

      - name: cloudfront invalidation
        run: |
            aws cloudfront create-invalidation --distribution-id E3U2HCN2ZRTBZN --paths "/index.html"
        env: # Or as an environment variable
          AWS_ACCESS_KEY_ID: $
          AWS_SECRET_ACCESS_KEY: $
          AWS_DEFAULT_REGION: $
      - name: Post to Mastodon
        run: | 
            export LATESTFILE=`ls -l _posts/ | grep ".* 20[0-9][0-9]-[0-9][0-9].*markdown" | tail -n1 | sed 's/.* \(20[0-9][0-9]-[0-9][0-9].*markdown\)/\1/'`
            export LATESTURL=`ls -l _posts/ | grep ".* 20[0-9][0-9]-[0-9][0-9].*markdown" | tail -n1 | sed 's/.* \(20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-\)\(.*\)\.markdown/\2.html/'`
            echo '{"status":"' | tr -d '\n' > payload.json
            cat _posts/$LATESTFILE | grep "^social: " | head -n 1 | sed 's/^social: "\(.*\)"/\1/' | tr -d '\n'>> payload.json
            cat _posts/$LATESTFILE | grep "^date: " | head -n 1 | sed "s/^date: .\([0-9]*\)-\([0-9]*\)-\([0-9]*\) .*/ https:\/\/freshbrewed.science\/\1\/\2\/\3\/$LATESTURL/g" | sed 's/.markdown/.html/' | tr -d '\n' >> payload.json
            echo '"}' >> payload.json

            cat payload.json | base64

            curl -X POST -H "Authorization: Bearer $MASTODONAPI" -H 'Content-Type: application/json' -d @payload.json https://noc.social/api/v1/statuses
        env:
          MASTODONAPI: $
          GHTOKEN: $
      - name: Build count
        uses: masci/datadog@v1
        with:
          api-key: $

Summary

Today we dug into Dapr Secrets using AKV and GCP Secrets Manager. We showed how to set a value and use them in a container. We then took a pivot to look at Mastodon and how one can post status messages using the REST API. We wrapped up by updating our Github Actions workflow to use the API Token to pull a YAML block and post on our behalf when new Blog entries are released.

Dapr Mastodon

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes