Merge pull request #4745 from github-vincent-miszczak/aws-sd-tags

feat(aws-sd): tag services
This commit is contained in:
Kubernetes Prow Robot 2024-10-19 22:33:04 +01:00 committed by GitHub
commit b834fef2b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 141 additions and 6 deletions

View File

@ -12,7 +12,7 @@ Learn more about the API in the [AWS Cloud Map API Reference](https://docs.aws.a
## IAM Permissions
To use the AWS Cloud Map API, a user must have permissions to create the DNS namespace. Additionally you need to make sure that your nodes (on which External DNS runs) have an IAM instance profile with the `AWSCloudMapFullAccess` managed policy attached, that provides following permissions:
To use the AWS Cloud Map API, a user must have permissions to create the DNS namespace. You need to make sure that your nodes (on which External DNS runs) have an IAM instance profile with the `AWSCloudMapFullAccess` managed policy attached, that provides following permissions:
```
{
@ -42,6 +42,82 @@ To use the AWS Cloud Map API, a user must have permissions to create the DNS nam
}
```
### IAM Permissions with ABAC
You can use Attribute-based access control(ABAC) for advanced deployments.
You can define AWS tags that are applied to services created by the controller. By doing so, you can have precise control over your IAM policy to limit the scope of the permissions to services managed by the controller, rather than having to grant full permissions on your entire AWS account.
To pass tags to service creation, use either CLI flags or environment variables:
*cli:* `--aws-sd-create-tag=key1=value1 --aws-sd-create-tag=key2=value2`
*environment:* `EXTERNAL_DNS_AWS_SD_CREATE_TAG=key1=value1\nkey2=value2`
Using tags, your `servicediscovery` policy can become:
```
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"servicediscovery:ListNamespaces",
"servicediscovery:ListServices"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"servicediscovery:CreateService",
"servicediscovery:TagResource"
],
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"aws:RequestTag/YOUR_TAG_KEY": "YOUR_TAG_VALUE"
}
}
},
{
"Effect": "Allow",
"Action": [
"servicediscovery:DiscoverInstances"
],
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"servicediscovery:NamespaceName": "YOUR_NAMESPACE_NAME"
}
}
},
{
"Effect": "Allow",
"Action": [
"servicediscovery:RegisterInstance",
"servicediscovery:DeregisterInstance",
"servicediscovery:DeleteService",
"servicediscovery:UpdateService"
],
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"aws:ResourceTag/YOUR_TAG_KEY": "YOUR_TAG_VALUE"
}
}
}
]
}
```
## Set up a namespace
Create a DNS namespace using the AWS Cloud Map API:

View File

@ -234,7 +234,7 @@ func main() {
log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
cfg.Registry = "aws-sd"
}
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, sd.NewFromConfig(aws.CreateDefaultV2Config(cfg)))
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, cfg.AWSSDCreateTag, 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.AzureZonesCacheDuration, cfg.DryRun)
case "azure-private-dns":

View File

@ -96,6 +96,7 @@ type Config struct {
AWSPreferCNAME bool
AWSZoneCacheDuration time.Duration
AWSSDServiceCleanup bool
AWSSDCreateTag map[string]string
AWSZoneMatchParent bool
AWSDynamoDBRegion string
AWSDynamoDBTable string
@ -257,6 +258,7 @@ var defaultConfig = &Config{
AWSPreferCNAME: false,
AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false,
AWSSDCreateTag: map[string]string{},
AWSDynamoDBRegion: "",
AWSDynamoDBTable: "external-dns",
AzureConfigFile: "/etc/kubernetes/azure.json",
@ -359,7 +361,9 @@ var defaultConfig = &Config{
// NewConfig returns new Config object
func NewConfig() *Config {
return &Config{}
return &Config{
AWSSDCreateTag: map[string]string{},
}
}
func (cfg *Config) String() string {
@ -477,6 +481,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("aws-zones-cache-duration", "When using the AWS provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AWSZoneCacheDuration.String()).DurationVar(&cfg.AWSZoneCacheDuration)
app.Flag("aws-zone-match-parent", "Expand limit possible target by sub-domains (default: disabled)").BoolVar(&cfg.AWSZoneMatchParent)
app.Flag("aws-sd-service-cleanup", "When using the AWS CloudMap provider, delete empty Services without endpoints (default: disabled)").BoolVar(&cfg.AWSSDServiceCleanup)
app.Flag("aws-sd-create-tag", "When using the AWS CloudMap provider, add tag to created services. The flag can be used multiple times").StringMapVar(&cfg.AWSSDCreateTag)
app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure)").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile)
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)

View File

@ -68,6 +68,7 @@ var (
AWSProfiles: []string{""},
AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false,
AWSSDCreateTag: map[string]string{},
AWSDynamoDBTable: "external-dns",
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
@ -167,6 +168,7 @@ var (
AWSProfiles: []string{"profile1", "profile2"},
AWSZoneCacheDuration: 10 * time.Second,
AWSSDServiceCleanup: true,
AWSSDCreateTag: map[string]string{"key1": "value1", "key2": "value2"},
AWSDynamoDBTable: "custom-table",
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
@ -325,6 +327,8 @@ func TestParseFlags(t *testing.T) {
"--aws-profile=profile2",
"--aws-zones-cache-duration=10s",
"--aws-sd-service-cleanup",
"--aws-sd-create-tag=key1=value1",
"--aws-sd-create-tag=key2=value2",
"--no-aws-evaluate-target-health",
"--policy=upsert-only",
"--registry=noop",
@ -436,6 +440,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AWS_PROFILE": "profile1\nprofile2",
"EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s",
"EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true",
"EXTERNAL_DNS_AWS_SD_CREATE_TAG": "key1=value1\nkey2=value2",
"EXTERNAL_DNS_DYNAMODB_TABLE": "custom-table",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
@ -504,8 +509,8 @@ func restoreEnv(t *testing.T, originalEnv map[string]string) {
func TestPasswordsNotLogged(t *testing.T) {
cfg := Config{
PDNSAPIKey: "pdns-api-key",
RFC2136TSIGSecret: "tsig-secret",
PDNSAPIKey: "pdns-api-key",
RFC2136TSIGSecret: "tsig-secret",
}
s := cfg.String()

View File

@ -79,10 +79,12 @@ type AWSSDProvider struct {
cleanEmptyService bool
// filter services for removal
ownerID string
// tags to be added to the service
tags []sdtypes.Tag
}
// NewAWSSDProvider initializes a new AWS Cloud Map based Provider.
func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, dryRun, cleanEmptyService bool, ownerID string, client AWSSDClient) (*AWSSDProvider, error) {
func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, dryRun, cleanEmptyService bool, ownerID string, tags map[string]string, client AWSSDClient) (*AWSSDProvider, error) {
p := &AWSSDProvider{
client: client,
dryRun: dryRun,
@ -90,6 +92,7 @@ func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string,
namespaceTypeFilter: newSdNamespaceFilter(namespaceType),
cleanEmptyService: cleanEmptyService,
ownerID: ownerID,
tags: awsTags(tags),
}
return p, nil
@ -113,6 +116,15 @@ func newSdNamespaceFilter(namespaceTypeConfig string) sdtypes.NamespaceFilter {
}
}
// awsTags converts user supplied tags to AWS format
func awsTags(tags map[string]string) []sdtypes.Tag {
awsTags := make([]sdtypes.Tag, 0, len(tags))
for k, v := range tags {
awsTags = append(awsTags, sdtypes.Tag{Key: aws.String(k), Value: aws.String(v)})
}
return awsTags
}
// Records returns list of all endpoints.
func (p *AWSSDProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
namespaces, err := p.ListNamespaces(ctx)
@ -400,6 +412,7 @@ func (p *AWSSDProvider) CreateService(ctx context.Context, namespaceID *string,
}},
},
NamespaceId: namespaceID,
Tags: p.tags,
})
if err != nil {
return nil, err

View File

@ -900,3 +900,39 @@ func TestAWSSDProvider_DeregisterInstance(t *testing.T) {
assert.Len(t, instances["srv1"], 0)
}
func TestAWSSDProvider_awsTags(t *testing.T) {
tests := []struct {
Expectation []sdtypes.Tag
Input map[string]string
}{
{
Expectation: []sdtypes.Tag{
{
Key: aws.String("key1"),
Value: aws.String("value1"),
},
{
Key: aws.String("key2"),
Value: aws.String("value2"),
},
},
Input: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
{
Expectation: []sdtypes.Tag{},
Input: map[string]string{},
},
{
Expectation: []sdtypes.Tag{},
Input: nil,
},
}
for _, test := range tests {
assert.EqualValues(t, test.Expectation, awsTags(test.Input))
}
}