crazy-max / ddns-route53

Dynamic DNS for Amazon Route 53 on a time-based schedule
https://crazymax.dev/ddns-route53/
MIT License
280 stars 38 forks source link

Documentation: one-shot mode #1164

Open stevecrozz opened 8 months ago

stevecrozz commented 8 months ago

Description

Update the documentation so that readers will know that the program does not need to run as a daemon. That it runs in a one-shot mode when schedule is not provided.

javydekoning commented 7 months ago

Yeah, I'd like that as well! Not passing in a schedule should do a one-shot run.

I have k8s handle the scheduling for me. The way I handle this today is like this:

apiVersion: v1
metadata:
  name: r53creds
data:
  HOSTED_ZONE_ID: 
  RECORD_NAME: 
  AWS_ACCESS_KEY_ID:
  AWS_SECRET_ACCESS_KEY:
  AWS_DEFAULT_REGION:
kind: Secret
type: Opaque
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: ddns
spec:
  schedule: "*/15 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: ddns
            image: alpine
            imagePullPolicy: IfNotPresent
            envFrom:
              - secretRef:
                  name: r53creds
            command: ["/bin/sh", "-c"]
            args:
              - |-
                apk add aws-cli curl jq --no-cache && \
                RECORD_TYPE="A" && \
                TTL=300 && \
                NEW_IP_ADDRESS=$(curl https://ipv4.icanhazip.com/) && \
                # Define the JSON in a multi-line variable
                CHANGE_BATCH=$(cat <<EOF
                {
                  "Changes": [
                    {
                      "Action": "UPSERT",
                      "ResourceRecordSet": {
                        "Name": "${RECORD_NAME}",
                        "Type": "${RECORD_TYPE}",
                        "TTL": ${TTL},
                        "ResourceRecords": [
                          {"Value": "${NEW_IP_ADDRESS}"}
                        ]
                      }
                    }
                  ]
                }
                EOF
                )

                aws route53 change-resource-record-sets \
                    --hosted-zone-id "$HOSTED_ZONE_ID" \
                    --change-batch "$CHANGE_BATCH" | jq -r '.ChangeInfo.Status'

Which works, but is less pretty ;-)

stevecrozz commented 7 months ago

I experimented a bit yesterday and found ddns-route53 already does behave the way I want. It seems ddns-route53 does run in a one-shot mode when a schedule is not provided. I updated the issue title and description to indicate this is a documentation task.

javydekoning commented 7 months ago

Can you include a quick example? Then I'll happily open a pull request tomorrow.

stevecrozz commented 7 months ago

Sure. Looking at how it works, it seems the program runs one time right away, and then if there is no schedule, the program exits.

I think I would start with a note here in the online --help menu: https://github.com/crazy-max/ddns-route53/blob/eae4181d45d514eb4ac9cc7a8b96a8067fc950f0/docs/usage/cli.md?plain=1#L22

-      --schedule=STRING         CRON expression format ($SCHEDULE).
+      --schedule=STRING         CRON expression format ($SCHEDULE). Leave empty to run once and exit.
javydekoning commented 7 months ago

Ok I have adopted this in k8s. Works like a charm:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: ddns-route53
spec:
  schedule: "*/15 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: ddns-route53
              image: crazymax/ddns-route53:latest
              imagePullPolicy: IfNotPresent
              env:
                - name: TZ
                  value: "Europe/Amsterdam"
                - name: LOG_LEVEL
                  value: info
                - name: LOG_JSON
                  value: "false"
              volumeMounts:
                - name: secret-volume
                  readOnly: true
                  mountPath: "/ddns-route53.yml"
                  subPath: ddns-route53.yml
          volumes:
            - name: secret-volume
              secret:
                secretName: route-53-credentials-ddns

My secret:

apiVersion: v1
kind: Secret
type: Opaque
metadata:
  name: route-53-credentials-ddns
  namespace: default
data:
  ddns-route53.yml: <removed>

If you want to load this secret from AWS Secrets Manger through external secrets consider this:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: external-route-53-credentials-ddns
  namespace: default
spec:
  dataFrom:
  - extract:
      key: k8s-route-53-credentials
  refreshInterval: 1h
  secretStoreRef:
    kind: ClusterSecretStore
    name: aws-secretsmanager
  target:
    creationPolicy: Owner
    name: route-53-credentials-ddns
    template:
      data:
        ddns-route53.yml: |
          credentials:
            accessKeyID: "{{ .AWS_ACCESS_KEY_ID }}"
            secretAccessKey: "{{ .AWS_SECRET_ACCESS_KEY }}"

          route53:
            hostedZoneID: "{{ .HOSTED_ZONE_ID }}"
            recordsSet:
              - name: "{{ .RECORD_NAME }}"
                type: "A"
                ttl: 300
      mergePolicy: Replace