Skip to content

Commit

Permalink
Merge pull request #4745 from github-vincent-miszczak/aws-sd-tags
Browse files Browse the repository at this point in the history
feat(aws-sd): tag services
  • Loading branch information
k8s-ci-robot authored Oct 19, 2024
2 parents 60ba543 + af23142 commit b834fef
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 6 deletions.
78 changes: 77 additions & 1 deletion docs/tutorials/aws-sd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
{
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
7 changes: 6 additions & 1 deletion pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ type Config struct {
AWSPreferCNAME bool
AWSZoneCacheDuration time.Duration
AWSSDServiceCleanup bool
AWSSDCreateTag map[string]string
AWSZoneMatchParent bool
AWSDynamoDBRegion string
AWSDynamoDBTable string
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 7 additions & 2 deletions pkg/apis/externaldns/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: "",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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()
Expand Down
15 changes: 14 additions & 1 deletion provider/awssd/aws_sd.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,20 @@ 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,
namespaceFilter: domainFilter,
namespaceTypeFilter: newSdNamespaceFilter(namespaceType),
cleanEmptyService: cleanEmptyService,
ownerID: ownerID,
tags: awsTags(tags),
}

return p, nil
Expand All @@ -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)
Expand Down Expand Up @@ -400,6 +412,7 @@ func (p *AWSSDProvider) CreateService(ctx context.Context, namespaceID *string,
}},
},
NamespaceId: namespaceID,
Tags: p.tags,
})
if err != nil {
return nil, err
Expand Down
36 changes: 36 additions & 0 deletions provider/awssd/aws_sd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

0 comments on commit b834fef

Please sign in to comment.