Skip to content

Commit

Permalink
Merge pull request #4811 from tanujd11/feat/azure-zone-list-cache
Browse files Browse the repository at this point in the history
feat: add azure zone list cache
  • Loading branch information
k8s-ci-robot authored Oct 18, 2024
2 parents 7fc4814 + 55839f5 commit 60ba543
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 7 deletions.
4 changes: 4 additions & 0 deletions docs/tutorials/azure-private-dns.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ $ az role assignment create --role "Reader" --assignee <appId GUID> --scope <res
$ az role assignment create --role "Private DNS Zone Contributor" --assignee <appId GUID> --scope <dns zone resource id>
```

## Throttling

When the ExternalDNS managed zones list doesn't change frequently, one can set `--azure-zones-cache-duration` (zones list cache time-to-live). The zones list cache is disabled by default, with a value of 0s.

## Deploy ExternalDNS
Configure `kubectl` to be able to communicate and authenticate with your cluster.
This is per default done through the file `~/.kube/config`.
Expand Down
4 changes: 4 additions & 0 deletions docs/tutorials/azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,10 @@ NOTE: it's also possible to specify (or override) ClientID through `userAssigned

NOTE: make sure the pod is restarted whenever you make a configuration change.

## Throttling

When the ExternalDNS managed zones list doesn't change frequently, one can set `--azure-zones-cache-duration` (zones list cache time-to-live). The zones list cache is disabled by default, with a value of 0s.

## Ingress used with ExternalDNS

This deployment assumes that you will be using nginx-ingress. When using nginx-ingress do not deploy it as a Daemon Set. This causes nginx-ingress to write the Cluster IP of the backend pods in the ingress status.loadbalancer.ip property which then has external-dns write the Cluster IP(s) in DNS vs. the nginx-ingress service external IP.
Expand Down
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,9 @@ func main() {
}
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, sd.NewFromConfig(aws.CreateDefaultV2Config(cfg)))
case "azure-dns", "azure":
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.DryRun)
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.DryRun)
case "azure-private-dns":
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.DryRun)
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureSubscriptionID, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.AzureActiveDirectoryAuthorityHost, cfg.AzureZonesCacheDuration, cfg.DryRun)
case "ultradns":
p, err = ultradns.NewUltraDNSProvider(domainFilter, cfg.DryRun)
case "civo":
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type Config struct {
AzureSubscriptionID string
AzureUserAssignedIdentityClientID string
AzureActiveDirectoryAuthorityHost string
AzureZonesCacheDuration time.Duration
CloudflareProxied bool
CloudflareDNSRecordsPerPage int
CoreDNSPrefix string
Expand Down Expand Up @@ -261,6 +262,7 @@ var defaultConfig = &Config{
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
AzureZonesCacheDuration: 0 * time.Second,
CloudflareProxied: false,
CloudflareDNSRecordsPerPage: 100,
CoreDNSPrefix: "/skydns/",
Expand Down Expand Up @@ -479,6 +481,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
app.Flag("azure-subscription-id", "When using the Azure provider, override the Azure subscription to use (optional)").Default(defaultConfig.AzureSubscriptionID).StringVar(&cfg.AzureSubscriptionID)
app.Flag("azure-user-assigned-identity-client-id", "When using the Azure provider, override the client id of user assigned identity in config file (optional)").Default("").StringVar(&cfg.AzureUserAssignedIdentityClientID)
app.Flag("azure-zones-cache-duration", "When using the Azure provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AzureZonesCacheDuration.String()).DurationVar(&cfg.AzureZonesCacheDuration)
app.Flag("tencent-cloud-config-file", "When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud)").Default(defaultConfig.TencentCloudConfigFile).StringVar(&cfg.TencentCloudConfigFile)
app.Flag("tencent-cloud-zone-type", "When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private)").Default(defaultConfig.TencentCloudZoneType).EnumVar(&cfg.TencentCloudZoneType, "", "public", "private")

Expand Down
12 changes: 10 additions & 2 deletions provider/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"context"
"fmt"
"strings"
"time"

log "github.com/sirupsen/logrus"

Expand Down Expand Up @@ -60,13 +61,14 @@ type AzureProvider struct {
userAssignedIdentityClientID string
activeDirectoryAuthorityHost string
zonesClient ZonesClient
zonesCache *zonesCache[dns.Zone]
recordSetsClient RecordSetsClient
}

// NewAzureProvider creates a new Azure provider.
//
// Returns the provider or an error if a provider could not be created.
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, dryRun bool) (*AzureProvider, error) {
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesCacheDuration time.Duration, dryRun bool) (*AzureProvider, error) {
cfg, err := getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
Expand All @@ -93,6 +95,7 @@ func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zon
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
activeDirectoryAuthorityHost: cfg.ActiveDirectoryAuthorityHost,
zonesClient: zonesClient,
zonesCache: &zonesCache[dns.Zone]{duration: zonesCacheDuration},
recordSetsClient: recordSetsClient,
}, nil
}
Expand Down Expand Up @@ -167,6 +170,10 @@ func (p *AzureProvider) ApplyChanges(ctx context.Context, changes *plan.Changes)

func (p *AzureProvider) zones(ctx context.Context) ([]dns.Zone, error) {
log.Debugf("Retrieving Azure DNS zones for resource group: %s.", p.resourceGroup)
if !p.zonesCache.Expired() {
log.Debugf("Using cached Azure DNS zones for resource group: %s zone count: %d.", p.resourceGroup, len(p.zonesCache.Get()))
return p.zonesCache.Get(), nil
}
var zones []dns.Zone
pager := p.zonesClient.NewListByResourceGroupPager(p.resourceGroup, &dns.ZonesClientListByResourceGroupOptions{Top: nil})
for pager.More() {
Expand All @@ -183,7 +190,8 @@ func (p *AzureProvider) zones(ctx context.Context) ([]dns.Zone, error) {
}
}
}
log.Debugf("Found %d Azure DNS zone(s).", len(zones))
log.Debugf("Found %d Azure DNS zone(s). Updating zones cache", len(zones))
p.zonesCache.Reset(zones)
return zones, nil
}

Expand Down
13 changes: 10 additions & 3 deletions provider/azure/azure_private_dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"context"
"fmt"
"strings"
"time"

azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
Expand Down Expand Up @@ -55,13 +56,14 @@ type AzurePrivateDNSProvider struct {
userAssignedIdentityClientID string
activeDirectoryAuthorityHost string
zonesClient PrivateZonesClient
zonesCache *zonesCache[privatedns.PrivateZone]
recordSetsClient PrivateRecordSetsClient
}

// NewAzurePrivateDNSProvider creates a new Azure Private DNS provider.
//
// Returns the provider or an error if a provider could not be created.
func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, dryRun bool) (*AzurePrivateDNSProvider, error) {
func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, subscriptionID string, resourceGroup string, userAssignedIdentityClientID string, activeDirectoryAuthorityHost string, zonesCacheDuration time.Duration, dryRun bool) (*AzurePrivateDNSProvider, error) {
cfg, err := getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID, activeDirectoryAuthorityHost)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
Expand All @@ -88,6 +90,7 @@ func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainF
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
activeDirectoryAuthorityHost: cfg.ActiveDirectoryAuthorityHost,
zonesClient: zonesClient,
zonesCache: &zonesCache[privatedns.PrivateZone]{duration: zonesCacheDuration},
recordSetsClient: recordSetsClient,
}, nil
}
Expand Down Expand Up @@ -177,7 +180,10 @@ func (p *AzurePrivateDNSProvider) ApplyChanges(ctx context.Context, changes *pla

func (p *AzurePrivateDNSProvider) zones(ctx context.Context) ([]privatedns.PrivateZone, error) {
log.Debugf("Retrieving Azure Private DNS zones for Resource Group '%s'", p.resourceGroup)

if !p.zonesCache.Expired() {
log.Debugf("Using cached Azure Private DNS zones for resource group: %s zone count: %d.", p.resourceGroup, len(p.zonesCache.Get()))
return p.zonesCache.Get(), nil
}
var zones []privatedns.PrivateZone

pager := p.zonesClient.NewListByResourceGroupPager(p.resourceGroup, &privatedns.PrivateZonesClientListByResourceGroupOptions{Top: nil})
Expand All @@ -198,7 +204,8 @@ func (p *AzurePrivateDNSProvider) zones(ctx context.Context) ([]privatedns.Priva
}
}

log.Debugf("Found %d Azure Private DNS zone(s).", len(zones))
log.Debugf("Found %d Azure Private DNS zone(s). Updating zones cache", len(zones))
p.zonesCache.Reset(zones)
return zones, nil
}

Expand Down
1 change: 1 addition & 0 deletions provider/azure/azure_privatedns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneNameFilt
dryRun: dryRun,
resourceGroup: resourceGroup,
zonesClient: privateZonesClient,
zonesCache: &zonesCache[privatedns.PrivateZone]{duration: 0},
recordSetsClient: privateRecordsClient,
}
}
Expand Down
1 change: 1 addition & 0 deletions provider/azure/azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ func newAzureProvider(domainFilter endpoint.DomainFilter, zoneNameFilter endpoin
userAssignedIdentityClientID: userAssignedIdentityClientID,
activeDirectoryAuthorityHost: activeDirectoryAuthorityHost,
zonesClient: zonesClient,
zonesCache: &zonesCache[dns.Zone]{duration: 0},
recordSetsClient: recordsClient,
}
}
Expand Down
50 changes: 50 additions & 0 deletions provider/azure/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azure

import (
"time"
)

// zonesCache is a cache for Azure zones(private or public)
type zonesCache[T any] struct {
age time.Time
duration time.Duration
zones []T
}

// Reset method to reset the zones and update the age. This will be used to update the cache
// after making a new API call to get the zones.
func (z *zonesCache[T]) Reset(zones []T) {
if z.duration > time.Duration(0) {
z.age = time.Now()
z.zones = zones
}
}

// Get method to retrieve the cached zones. If cache is not expired, this will be used
// instead of making a new API call to get the zones.
func (z *zonesCache[T]) Get() []T {
return z.zones
}

// Expired method to check if the cache has expired based on duration or if zones are empty.
// If cache is expired, a new API call will be made to get the zones. If zones are empty, a new
// API call will be made to get the zones. This case comes in at the time of initialization.
func (z *zonesCache[T]) Expired() bool {
return len(z.zones) < 1 || time.Since(z.age) > z.duration
}
78 changes: 78 additions & 0 deletions provider/azure/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azure

import (
"testing"
"time"

dns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns"
"github.com/stretchr/testify/assert"
)

func TestzonesCache(t *testing.T) {
now := time.Now()
zoneName := "example.com"
var testCases = map[string]struct {
z *zonesCache[dns.Zone]
expired bool
}{
"inactive-zone-cache": {
&zonesCache[dns.Zone]{
duration: 0 * time.Second,
},
true,
},
"empty-active-zone-cache": {
&zonesCache[dns.Zone]{
duration: 30 * time.Second,
},
true,
},
"expired-zone-cache": {
&zonesCache[dns.Zone]{
age: now.Add(-300 * time.Second),
duration: 30 * time.Second,
},
true,
},
"active-zone-cache": {
&zonesCache[dns.Zone]{
zones: []dns.Zone{{
Name: &zoneName,
}},
duration: 30 * time.Second,
age: now,
},
false,
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
assert.Equal(t, testCase.expired, testCase.z.Expired())
var resetZoneLength = 1
if testCase.z.duration == 0 {
resetZoneLength = 0
}
testCase.z.Reset([]dns.Zone{{
Name: &zoneName,
}})
assert.Len(t, testCase.z.Get(), resetZoneLength)
})
}
}

0 comments on commit 60ba543

Please sign in to comment.