agent: add RetryJoin support for Azure

Pull #2978 from leowmjw/develop

Resolves #2978
This commit is contained in:
Michael Leow 2017-05-24 09:19:23 +02:00 committed by Frank Schroeder
parent 37e159df63
commit ec81c18006
No known key found for this signature in database
GPG Key ID: 4D65C6EAEC87DECD
9 changed files with 213 additions and 5 deletions

View File

@ -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.")
@ -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
}
@ -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...)

View File

@ -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)

View File

@ -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
@ -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"`
@ -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
}

View File

@ -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
}

View File

@ -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)))

View File

@ -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.
@ -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).

View File

@ -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.

View File

@ -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.

View File

@ -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