Skip to content

Commit

Permalink
agent: add RetryJoin support for Azure
Browse files Browse the repository at this point in the history
Pull #2978 from leowmjw/develop

Resolves #2978
  • Loading branch information
leowmjw authored and magiconair committed May 24, 2017
1 parent c770d7e commit deb206b
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 5 deletions.
18 changes: 15 additions & 3 deletions command/agent/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ func (c *Command) readConfig() *Config {
"Google Compute Engine tag value to filter on for server discovery.")
f.StringVar(&cmdConfig.RetryJoinGCE.CredentialsFile, "retry-join-gce-credentials-file", "",
"Path to credentials JSON file to use with Google Compute Engine.")
f.StringVar(&cmdConfig.RetryJoinAzure.TagName, "retry-join-azure-tag-name", "",
"Azure tag name to filter on for server discovery.")
f.StringVar(&cmdConfig.RetryJoinAzure.TagValue, "retry-join-azure-tag-value", "",
"Azure tag value to filter on for server discovery.")
f.Var((*AppendSliceValue)(&cmdConfig.RetryJoinWan), "retry-join-wan",
"Address of an agent to join -wan at start time with retries enabled. "+
"Can be specified multiple times.")
Expand Down Expand Up @@ -570,8 +574,10 @@ func (c *Command) startupJoinWan(config *Config) error {
// retries are exhausted.
func (c *Command) retryJoin(config *Config, errCh chan<- struct{}) {
ec2Enabled := config.RetryJoinEC2.TagKey != "" && config.RetryJoinEC2.TagValue != ""
gceEnabled := config.RetryJoinGCE.TagValue != ""
azureEnabled := config.RetryJoinAzure.TagName != "" && config.RetryJoinAzure.TagValue != ""

if len(config.RetryJoin) == 0 && !ec2Enabled && config.RetryJoinGCE.TagValue == "" {
if len(config.RetryJoin) == 0 && !ec2Enabled && !gceEnabled && !azureEnabled {
return
}

Expand All @@ -589,12 +595,18 @@ func (c *Command) retryJoin(config *Config, errCh chan<- struct{}) {
logger.Printf("[ERROR] agent: Unable to query EC2 instances: %s", err)
}
logger.Printf("[INFO] agent: Discovered %d servers from EC2", len(servers))
case config.RetryJoinGCE.TagValue != "":
case gceEnabled:
servers, err = config.discoverGCEHosts(logger)
if err != nil {
logger.Printf("[ERROR] agent: Unable to query GCE insances: %s", err)
logger.Printf("[ERROR] agent: Unable to query GCE instances: %s", err)
}
logger.Printf("[INFO] agent: Discovered %d servers from GCE", len(servers))
case azureEnabled:
servers, err = config.discoverAzureHosts(logger)
if err != nil {
logger.Printf("[ERROR] agent: Unable to query Azure instances: %s", err)
}
logger.Printf("[INFO] agent: Discovered %d servers from Azure", len(servers))
}

servers = append(servers, config.RetryJoin...)
Expand Down
36 changes: 36 additions & 0 deletions command/agent/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,42 @@ func TestDiscoverGCEHosts(t *testing.T) {
}
}

func TestDiscoverAzureHosts(t *testing.T) {
subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID")
tenantID := os.Getenv("ARM_TENANT_ID")
clientID := os.Getenv("ARM_CLIENT_ID")
clientSecret := os.Getenv("ARM_CLIENT_SECRET")
environment := os.Getenv("ARM_ENVIRONMENT")

if subscriptionID == "" || clientID == "" || clientSecret == "" || tenantID == "" {
t.Skip("ARM_SUBSCRIPTION_ID, ARM_CLIENT_ID, ARM_CLIENT_SECRET and ARM_TENANT_ID " +
"must be set to test Discover Azure Hosts")
}

if environment == "" {
t.Log("Environments other than Public not supported at the moment")
}

c := &Config{
RetryJoinAzure: RetryJoinAzure{
SubscriptionID: subscriptionID,
ClientID: clientID,
SecretAccessKey: clientSecret,
TenantID: tenantID,
TagName: "type",
TagValue: "Foundation",
},
}

servers, err := c.discoverAzureHosts(log.New(os.Stderr, "", log.LstdFlags))
if err != nil {
t.Fatal(err)
}
if len(servers) != 3 {
t.Fatalf("bad: %v", servers)
}
}

func TestProtectDataDir(t *testing.T) {
dir := testutil.TempDir(t, "consul")
defer os.RemoveAll(dir)
Expand Down
33 changes: 33 additions & 0 deletions command/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@ type RetryJoinGCE struct {
CredentialsFile string `mapstructure:"credentials_file"`
}

// RetryJoinAzure is used to configure discovery of instances via AzureRM API
type RetryJoinAzure struct {
// The tag name and value to use when filtering instances
TagName string `mapstructure:"tag_name"`
TagValue string `mapstructure:"tag_value"`

// The Azure credentials to use for making requests to AzureRM
SubscriptionID string `mapstructure:"subscription_id" json:"-"`
TenantID string `mapstructure:"tenant_id" json:"-"`
ClientID string `mapstructure:"client_id" json:"-"`
SecretAccessKey string `mapstructure:"secret_access_key" json:"-"`
}

// Performance is used to tune the performance of Consul's subsystems.
type Performance struct {
// RaftMultiplier is an integer multiplier used to scale Raft timing
Expand Down Expand Up @@ -537,6 +550,8 @@ type Config struct {
// The config struct for the GCE tag server discovery feature.
RetryJoinGCE RetryJoinGCE `mapstructure:"retry_join_gce"`

RetryJoinAzure RetryJoinAzure `mapstructure:"retry_join_azure"`

// RetryJoinWan is a list of addresses to join -wan with retry enabled.
RetryJoinWan []string `mapstructure:"retry_join_wan"`

Expand Down Expand Up @@ -1728,6 +1743,24 @@ func MergeConfig(a, b *Config) *Config {
if b.RetryJoinGCE.CredentialsFile != "" {
result.RetryJoinGCE.CredentialsFile = b.RetryJoinGCE.CredentialsFile
}
if b.RetryJoinAzure.TagName != "" {
result.RetryJoinAzure.TagName = b.RetryJoinAzure.TagName
}
if b.RetryJoinAzure.TagValue != "" {
result.RetryJoinAzure.TagValue = b.RetryJoinAzure.TagValue
}
if b.RetryJoinAzure.SubscriptionID != "" {
result.RetryJoinAzure.SubscriptionID = b.RetryJoinAzure.SubscriptionID
}
if b.RetryJoinAzure.TenantID != "" {
result.RetryJoinAzure.TenantID = b.RetryJoinAzure.TenantID
}
if b.RetryJoinAzure.ClientID != "" {
result.RetryJoinAzure.ClientID = b.RetryJoinAzure.ClientID
}
if b.RetryJoinAzure.SecretAccessKey != "" {
result.RetryJoinAzure.SecretAccessKey = b.RetryJoinAzure.SecretAccessKey
}
if b.RetryMaxAttemptsWan != 0 {
result.RetryMaxAttemptsWan = b.RetryMaxAttemptsWan
}
Expand Down
58 changes: 58 additions & 0 deletions command/agent/config_azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package agent

import (
"fmt"
"log"
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
)

// discoverAzureHosts searches an Azure Subscription, returning a list of instance ips
// where AzureTag_Name = AzureTag_Value
func (c *Config) discoverAzureHosts(logger *log.Logger) ([]string, error) {
var servers []string
// Only works for the Azure PublicCLoud for now; no ability to test other Environment
oauthConfig, err := azure.PublicCloud.OAuthConfigForTenant(c.RetryJoinAzure.TenantID)
if err != nil {
return nil, err
}
// Get the ServicePrincipalToken for use searching the NetworkInterfaces
sbt, tokerr := azure.NewServicePrincipalToken(*oauthConfig,
c.RetryJoinAzure.ClientID,
c.RetryJoinAzure.SecretAccessKey,
azure.PublicCloud.ResourceManagerEndpoint,
)
if tokerr != nil {
return nil, tokerr
}
// Setup the client using autorest; followed the structure from Terraform
vmnet := network.NewInterfacesClient(c.RetryJoinAzure.SubscriptionID)
vmnet.Client.UserAgent = fmt.Sprint("Hashicorp-Consul")
vmnet.Authorizer = sbt
vmnet.Sender = autorest.CreateSender(autorest.WithLogging(logger))
// Get all Network interfaces across ResourceGroups unless there is a compelling reason to restrict
netres, neterr := vmnet.ListAll()
if neterr != nil {
return nil, neterr
}
// For now, ignore Primary interfaces, choose any PrivateIPAddress with the matching tags
for _, oneint := range *netres.Value {
// Make it a little more robust just in case there is actually no Tags
if oneint.Tags != nil {
if *(*oneint.Tags)[c.RetryJoinAzure.TagName] == c.RetryJoinAzure.TagValue {
// Make it a little more robust just in case IPConfigurations nil
if oneint.IPConfigurations != nil {
for _, onecfg := range *oneint.IPConfigurations {
// fmt.Println("Internal FQDN: ", *onecfg.Name, " IP: ", *onecfg.PrivateIPAddress)
// Only get the address if there is private IP address
if onecfg.PrivateIPAddress != nil {
servers = append(servers, *onecfg.PrivateIPAddress)
}
}
}
}
}
}
return servers, nil
}
41 changes: 41 additions & 0 deletions command/agent/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,47 @@ func TestRetryJoinGCE(t *testing.T) {
}
}

func TestRetryJoinAzure(t *testing.T) {
input := `{
"retry_join_azure": {
"tag_name": "type",
"tag_value": "Foundation",
"subscription_id": "klm-no",
"tenant_id": "fgh-ij",
"client_id": "abc-de",
"secret_access_key": "qwerty"
}}`

config, err := DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}

if config.RetryJoinAzure.TagName != "type" {
t.Fatalf("bad: %#v", config)
}

if config.RetryJoinAzure.TagValue != "Foundation" {
t.Fatalf("bad: %#v", config)
}

if config.RetryJoinAzure.SubscriptionID != "klm-no" {
t.Fatalf("bad: %#v", config)
}

if config.RetryJoinAzure.TenantID != "fgh-ij" {
t.Fatalf("bad: %#v", config)
}

if config.RetryJoinAzure.ClientID != "abc-de" {
t.Fatalf("bad: %#v", config)
}

if config.RetryJoinAzure.SecretAccessKey != "qwerty" {
t.Fatalf("bad: %#v", config)
}
}

func TestDecodeConfig_Performance(t *testing.T) {
input := `{"performance": { "raft_multiplier": 3 }}`
config, err := DecodeConfig(bytes.NewReader([]byte(input)))
Expand Down
25 changes: 25 additions & 0 deletions website/source/docs/agent/options.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,18 @@ will exit with an error at startup.
- If none of these exist and discovery is being run from a GCE instance, the
instance's configured service account will be used.

* <a name="_retry_join_azure_tag_name"></a><a href="#_retry_join_azure_tag_name">`-retry-join-azure-tag-name`
</a> - The Azure instance tag name to filter on. When used with
[`-retry-join-azure-tag-value`](#_retry_join_azure_tag_value), Consul will attempt to join Azure
instances with the given tag name and value on startup.
</br></br>For Azure authentication the following methods are supported, in order:
- Static credentials (from the config file)

The only permission needed is the ListAll method for NetworkInterfaces. It is recommended you make a dedicated key used only for auto-joining.

* <a name="_retry_join_azure_tag_value"></a><a href="#_retry_join_azure_tag_value">`-retry-join-azure-tag-value`
</a> - The Azure instance tag value to filter on.

* <a name="_retry_interval"></a><a href="#_retry_interval">`-retry-interval`</a> - Time
to wait between join attempts. Defaults to 30s.

Expand Down Expand Up @@ -850,6 +862,19 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
[`-retry-join-gce-credentials-file` command-line
flag](#_retry_join_gce_credentials_file).

* <a name="retry_join_azure"></a><a href="#retry_join_azure">`retry_join_azure`</a> - This is a nested object
that allows the setting of Azure-related [`-retry-join`](#_retry_join) options.
<br><br>
The following keys are valid:
* `tag_name` - The Azure instance tag name to filter on. Equivalent to the</br>
[`-retry-join-azure-tag-name` command-line flag](#_retry_join_azure_tag_name).
* `tag_value` - The Azure instance tag value to filter on. Equivalent to the</br>
[`-retry-join-azure-tag-value` command-line flag](#_retry_join_azure_tag_value).
* `subscription_id` - The Azure Subscription ID to use for authentication.
* `tenant_id` - The Azure Tenant ID to use for authentication.
* `client_id` - The Azure Client ID to use for authentication.
* `secret_access_key` - The Azure secret access key to use for authentication.

* <a name="retry_interval"></a><a href="#retry_interval">`retry_interval`</a> Equivalent to the
[`-retry-interval` command-line flag](#_retry_interval).

Expand Down
4 changes: 3 additions & 1 deletion website/source/docs/faq.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ For users on AWS the [-retry-join-ec2 configuration options](/docs/agent/options

For users on GCE the [-retry-join-gce configuration options](/docs/agent/options.html#_retry_join_gce_tag_value) allow bootstrapping by automatically discovering instances on Google Compute Engine by tag value at startup.

For users not on AWS or GCE the native [-join and retry-join functionality](/docs/agent/options.html#_join) can be used.
For users on Azure the [-retry-join-azure configuration options](/docs/agent/options.html#_retry_join_azure_tag_name) allow bootstrapping by automatically discovering Azure instances with a given tag name/value at startup.

For users not on AWS, GCE or Azure the native [-join and retry-join functionality](/docs/agent/options.html#_join) can be used.

Other features of Consul Enterprise, such as the UI and Alerts also have suitable open source alternatives.

Expand Down
1 change: 1 addition & 0 deletions website/source/docs/guides/bootstrapping.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ To trigger leader election, we must join these machines together and create a cl
- Manually specified list of machines with [-retry-join](https://www.consul.io/docs/agent/options.html#_retry_join) option
- Automatic AWS EC2 instance joining with the [-retry-join-ec2-*](https://www.consul.io/docs/agent/options.html#_retry_join_ec2_tag_key) options
- Automatic GCE instance joining with the [-retry-join-gce-*](https://www.consul.io/docs/agent/options.html#_retry_join_gce_tag_value) options
- Automatic Azure instance joining with the [-retry-join-azure-*](https://www.consul.io/docs/agent/options.html#_retry_join_azure_tag_name) options

Choose the method which best suits your environment and specific use case.

Expand Down
2 changes: 1 addition & 1 deletion website/source/intro/getting-started/join.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ learn about <em>one existing member</em>. After joining the cluster, the
agents gossip with each other to propagate full membership information.

## Auto-joining a Cluster on Start
Ideally, whenever a new node is brought up in your datacenter, it should automatically join the Consul cluster without human intervention. Consul facilitates auto-join by enabling the auto-discovery of instances in AWS or Google Cloud with a given tag key/value. To use the integration, add the [`retry_join_ec2`](/docs/agent/options.html?#retry_join_ec2) or the [`retry_join_gce`](/docs/agent/options.html?#retry_join_gce) nested object to your Consul configuration file. This will allow a new node to join the cluster without any hardcoded configuration. Alternatively, you can join a cluster at startup using the [`-join` flag](/docs/agent/options.html#_join) or [`start_join` setting](/docs/agent/options.html#start_join) with hardcoded addresses of other known Consul agents.
Ideally, whenever a new node is brought up in your datacenter, it should automatically join the Consul cluster without human intervention. Consul facilitates auto-join by enabling the auto-discovery of instances in AWS, Google Cloud or Azure with a given tag key/value. To use the integration, add the [`retry_join_ec2`](/docs/agent/options.html?#retry_join_ec2), [`retry_join_gce`](/docs/agent/options.html?#retry_join_gce) or the [`retry_join_azure`](/docs/agent/options.html?#retry_join_azure) nested object to your Consul configuration file. This will allow a new node to join the cluster without any hardcoded configuration. Alternatively, you can join a cluster at startup using the [`-join` flag](/docs/agent/options.html#_join) or [`start_join` setting](/docs/agent/options.html#start_join) with hardcoded addresses of other known Consul agents.

## Querying Nodes

Expand Down

0 comments on commit deb206b

Please sign in to comment.