* storage proposal v1 * few fixes, remove code * link issues * another caveat * fix typos, more details * small refactoring
7.1 KiB
Storage
Initial discussion - https://github.com/kubernetes-incubator/external-dns/issues/44
Purpose
One should not be afraid to use external-dns, because it can delete or overwrite the records preexisting in the DNS provider.
Why we need it?
DNS provider (AWS Route53, Google DNS, etc.) stores dns records which are created via various means. Integration of External-DNS should be safe and should not delete or overwrite the records which it is not responsible for. Moreover, it should certainly be possible for multiple kubernetes clusters to share the same hosted zone within the dns provider, additionally multiple external-dns instances inside the same cluster should be able to co-exist without messing with the same set of records.
Storage provides a persistent storage with information to track the records created by the external-dns.
This proposal introduces multiple possible implementation with the details depending on the setup.
Requirements and assumptions
- Pre-existing records should not be modified by external-dns
- External-dns instance only creates/modifies/deletes records which are created by this instance
- It should be possible to transfer the ownership to another external-dns instance
- Any integrated dns-provider should provide at least a single way to implement the storage
- Lifetime of the records should not be limited to lifetime of external-dns
- External-dns should have its identifier for marking the managed records -
storage-id
Types of storage
The following presents two ways to implement the storage, and we are planning to implement both for compatibility purposes.
TXT records
This implementation idea is borrowed from Mate
Each record created by external-dns is accompanied by the TXT record, which internally stores the external-dns identifier. For example, if external dns with storage-id="external-dns-1" record to be created with dns name foo.zone.org, external-dns will create a TXT record with the same dns name foo.zone.org and injected value of "external-dns-1". The transfer of ownership can be done by modifying the value of the TXT record. If no TXT record exists for the record or the value does not match its own storage-id, then external-dns will simply ignore it.
Goods
- Easy to guarantee cross-cluster ownership safety
- Data lifetime is not limited to cluster or external-dns lifetime
- Supported by major DNS providers
Bads
- TXT record cannot co-exist with CNAME records (this can be solved by creating a TXT record with another domain name, e.g.
foo.org->foo.txt.org) - Introduces complexity to the logic
- Difficult to do the transfer of ownership
- Too easy to mess up with manual modifications
ConfigMap
Store the state in the configmap. ConfigMap is created and managed by each external-dns individually, i.e. external-dns with storage-id=external-dns-1 will create and operate on extern-dns-1-storage ConfigMap. ConfigMap will store all the records present in the DNS provider as serialized JSON. For example:
kind: ConfigMap
apiVersion: v1
metadata:
creationTimestamp: 2016-03-09T19:14:38Z
name: external-dns-1-storage
namespace: same-as-external-dns-1
data:
records: "[{
\"dnsname\": \"foo.org\",
\"owner\": \"external-dns-1\",
\"target\": \"loadbalancer1.com\",
\"type\": \"A\"
}, {
\"dnsname\": \"bar.org\",
\"owner\": \"external-dns-2\",
\"target\": \"loadbalancer2.com\",
\"type\": \"A\"
}, {
\"dnsname\": \"unmanaged.org\",
\"owner\": \"\",
\"target\": \"loadbalancer2.com\",
\"type\": \"CNAME\"
}]"
ConfigMap will be periodically resynced with the dns provider by fetching the dns records and comparing it with the data currently stored and hence rebuilding the ownership information.
Goods
- Not difficult to implement and easy to do the ownership transfer
- ConfigMap is a first class citizen in kubernetes world
- Does not create dependency/restriction on DNS provider
- Cannot be easily messed with by other parties
Bads
- ConfigMap might be out of sync with dns provider state
- LifeTime is obviously limited to the cluster lifetime
- Not supported in older kubernetes clusters
- Bloated ConfigMap - cannot be paginated and will grow very big on DNS provider with thousands of records
Component integration
Components:
- Source - all endpoints ( collection of ingress, service[type=LoadBalancer] etc.)
- Plan - object responsible for the create of change lists in external-dns
- DNSProvider - interface to access the DNS provider API
A single loop iteration of external-dns operation:
- Get all endpoints ( collection ingress, service[type=LoadBalancer] etc.) into collection of
endpoints - Get storage
Records() - Pass
Records(including ownership information) and list of endpoints toPlanto do the calculation - Make a call to DNS provider with
Planprovided change list - If call succeeded pass the change list to storage
Assign()to mark the records that are created
Storage gets updated all the time via Poll. Plan does not call DNS provider directly. Good value of the Poll is to have simple rate limiting mechanism on DNS provider.
Notes:
- DNS Provider should use batch operations
- DNS Provider should be called with CREATE operation (not UPSERT!) when the record does not yet exist!
- Storage does not need to be in complete sync with DNS provider due to #2. Hence resolving the potential caveats of ConfigMap implementation
Implementation
Basic implementation of the storage interface:
- Storage has the dnsprovider object to retrieve the list of records, but it never makes the call to modify the records (think layer to help out with endpoint filtering)
- Record() - returns whatever is stored in the storage
- Assign(endpoints) - called when the records are registered with dns provider - hence storage need to mark its ownership. Therefore DNSProvider serves as a safe-guard from race conditions
- WaitForSync() - called in the beginning to populate the storage, in case of configmap would be the configmap creation and fetching the dns provider records
- Poll() - resync loop to stay-up-to-date with dns provider state
Example:
We will provide InMemoryStorage non-persistent storage which should help us with testing.
type InMemoryStorage struct {
registry dnsprovider.DNSProvider
zone string
owner string
cache map[string]*SharedEndpoint
sync.Mutex
}
func (im *InMemoryStorage) Poll(stopChan <-chan struct{}) {
for {
select {
case <-time.After(resyncPeriod):
err := im.refreshCache()
if err != nil {
log.Errorf("failed to refresh cache: %v", err)
}
case <-stopChan:
log.Infoln("terminating storage polling")
return
}
}
}
func (im *InMemoryStorage) refreshCache() error {
im.Lock()
defer im.Unlock()
records, err := im.registry.Records(im.zone)
...
im.cache = map[string]*SharedEndpoint{} //drop the current cache
for _, newCacheRecord := range newCache {
im.cache[newCacheRecord.DNSName] = newCacheRecord
}
return nil
}
Initial PR: https://github.com/kubernetes-incubator/external-dns/pull/57