Merge branch 'master' of github.com:hashicorp/terraform

This commit is contained in:
JT 2014-07-23 15:37:01 -07:00
commit f39c1265cb
61 changed files with 2771 additions and 964 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ bin/
config/y.go config/y.go
config/y.output config/y.output
vendor/ vendor/
website/.vagrant

View File

@ -0,0 +1,10 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/dnsimple"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(new(dnsimple.ResourceProvider))
}

View File

@ -0,0 +1 @@
package main

View File

@ -47,7 +47,6 @@ func resource_aws_db_security_group_create(
if err != nil { if err != nil {
return rs, err return rs, err
} }
log.Printf("%#v", rs.Attributes)
if _, ok := rs.Attributes["ingress.#"]; ok { if _, ok := rs.Attributes["ingress.#"]; ok {
ingresses := flatmap.Expand( ingresses := flatmap.Expand(

View File

@ -2,6 +2,7 @@ package aws
import ( import (
"log" "log"
"os"
"github.com/hashicorp/terraform/helper/config" "github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/multierror" "github.com/hashicorp/terraform/helper/multierror"
@ -26,14 +27,30 @@ type ResourceProvider struct {
} }
func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) { func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
v := &config.Validator{ type param struct {
Optional: []string{ env string
"access_key", key string
"secret_key", }
"region", params := []param{
}, {"AWS_REGION", "region"},
{"AWS_ACCESS_KEY", "access_key"},
{"AWS_SECRET_KEY", "secret_key"},
} }
var optional []string
var required []string
for _, p := range params {
if v := os.Getenv(p.env); v != "" {
optional = append(optional, p.key)
} else {
required = append(required, p.key)
}
}
v := &config.Validator{
Required: required,
Optional: optional,
}
return v.Validate(c) return v.Validate(c)
} }

View File

@ -0,0 +1,33 @@
package dnsimple
import (
"log"
"os"
"github.com/rubyist/go-dnsimple"
)
type Config struct {
Token string `mapstructure:"token"`
Email string `mapstructure:"email"`
}
// Client() returns a new client for accessing heroku.
//
func (c *Config) Client() (*dnsimple.DNSimpleClient, error) {
// If we have env vars set (like in the acc) tests,
// we need to override the values passed in here.
if v := os.Getenv("DNSIMPLE_EMAIL"); v != "" {
c.Email = v
}
if v := os.Getenv("DNSIMPLE_TOKEN"); v != "" {
c.Token = v
}
client := dnsimple.NewClient(c.Token, c.Email)
log.Printf("[INFO] DNSimple Client configured for user: %s", client.Email)
return client, nil
}

View File

@ -0,0 +1,163 @@
package dnsimple
import (
"fmt"
"log"
"strconv"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/diff"
"github.com/hashicorp/terraform/terraform"
"github.com/rubyist/go-dnsimple"
)
func resource_dnsimple_record_create(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
// Merge the diff into the state so that we have all the attributes
// properly.
rs := s.MergeDiff(d)
var err error
newRecord := dnsimple.Record{
Name: rs.Attributes["name"],
Content: rs.Attributes["value"],
RecordType: rs.Attributes["type"],
}
if attr, ok := rs.Attributes["ttl"]; ok {
newRecord.TTL, err = strconv.Atoi(attr)
if err != nil {
return nil, err
}
}
log.Printf("[DEBUG] record create configuration: %#v", newRecord)
rec, err := client.CreateRecord(rs.Attributes["domain"], newRecord)
if err != nil {
return nil, fmt.Errorf("Failed to create record: %s", err)
}
rs.ID = strconv.Itoa(rec.Id)
log.Printf("[INFO] record ID: %s", rs.ID)
return resource_dnsimple_record_update_state(rs, &rec)
}
func resource_dnsimple_record_update(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
panic("Cannot update record")
return nil, nil
}
func resource_dnsimple_record_destroy(
s *terraform.ResourceState,
meta interface{}) error {
p := meta.(*ResourceProvider)
client := p.client
log.Printf("[INFO] Deleting record: %s", s.ID)
rec, err := resource_dnsimple_record_retrieve(s.Attributes["domain"], s.ID, client)
if err != nil {
return err
}
err = rec.Delete(client)
if err != nil {
return fmt.Errorf("Error deleting record: %s", err)
}
return nil
}
func resource_dnsimple_record_refresh(
s *terraform.ResourceState,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
rec, err := resource_dnsimple_record_retrieve(s.Attributes["app"], s.ID, client)
if err != nil {
return nil, err
}
return resource_dnsimple_record_update_state(s, rec)
}
func resource_dnsimple_record_diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig,
meta interface{}) (*terraform.ResourceDiff, error) {
b := &diff.ResourceBuilder{
Attrs: map[string]diff.AttrType{
"domain": diff.AttrTypeCreate,
"name": diff.AttrTypeCreate,
"value": diff.AttrTypeUpdate,
"ttl": diff.AttrTypeCreate,
"type": diff.AttrTypeUpdate,
},
ComputedAttrs: []string{
"priority",
"domain_id",
},
}
return b.Diff(s, c)
}
func resource_dnsimple_record_update_state(
s *terraform.ResourceState,
rec *dnsimple.Record) (*terraform.ResourceState, error) {
s.Attributes["name"] = rec.Name
s.Attributes["value"] = rec.Content
s.Attributes["type"] = rec.RecordType
s.Attributes["ttl"] = strconv.Itoa(rec.TTL)
s.Attributes["priority"] = strconv.Itoa(rec.Priority)
s.Attributes["domain_id"] = strconv.Itoa(rec.DomainId)
return s, nil
}
func resource_dnsimple_record_retrieve(domain string, id string, client *dnsimple.DNSimpleClient) (*dnsimple.Record, error) {
intId, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
record, err := client.RetrieveRecord(domain, intId)
if err != nil {
return nil, fmt.Errorf("Error retrieving record: %s", err)
}
return &record, nil
}
func resource_dnsimple_record_validation() *config.Validator {
return &config.Validator{
Required: []string{
"domain",
"name",
"value",
"type",
},
Optional: []string{
"ttl",
},
}
}

View File

@ -0,0 +1,115 @@
package dnsimple
import (
"fmt"
"strconv"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rubyist/go-dnsimple"
)
func TestAccDNSimpleRecord_Basic(t *testing.T) {
var record dnsimple.Record
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDNSimpleRecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckDNSimpleRecordConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDNSimpleRecordExists("dnsimple_record.foobar", &record),
testAccCheckDNSimpleRecordAttributes(&record),
resource.TestCheckResourceAttr(
"dnsimple_record.foobar", "name", "terraform"),
resource.TestCheckResourceAttr(
"dnsimple_record.foobar", "domain", "jack.ly"),
resource.TestCheckResourceAttr(
"dnsimple_record.foobar", "value", "192.168.0.10"),
),
},
},
})
}
func testAccCheckDNSimpleRecordDestroy(s *terraform.State) error {
client := testAccProvider.client
for _, rs := range s.Resources {
if rs.Type != "dnsimple_record" {
continue
}
intId, err := strconv.Atoi(rs.ID)
if err != nil {
return err
}
_, err = client.RetrieveRecord(rs.Attributes["domain"], intId)
if err == nil {
return fmt.Errorf("Record still exists")
}
}
return nil
}
func testAccCheckDNSimpleRecordAttributes(record *dnsimple.Record) resource.TestCheckFunc {
return func(s *terraform.State) error {
if record.Content != "192.168.0.10" {
return fmt.Errorf("Bad content: %s", record.Content)
}
return nil
}
}
func testAccCheckDNSimpleRecordExists(n string, record *dnsimple.Record) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.ID == "" {
return fmt.Errorf("No Record ID is set")
}
client := testAccProvider.client
intId, err := strconv.Atoi(rs.ID)
if err != nil {
return err
}
foundRecord, err := client.RetrieveRecord(rs.Attributes["domain"], intId)
if err != nil {
return err
}
if strconv.Itoa(foundRecord.Id) != rs.ID {
return fmt.Errorf("Record not found")
}
*record = foundRecord
return nil
}
}
const testAccCheckDNSimpleRecordConfig_basic = `
resource "dnsimple_record" "foobar" {
domain = "jack.ly"
name = "terraform"
value = "192.168.0.10"
type = "A"
ttl = 3600
}`

View File

@ -0,0 +1,68 @@
package dnsimple
import (
"log"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/terraform"
"github.com/rubyist/go-dnsimple"
)
type ResourceProvider struct {
Config Config
client *dnsimple.DNSimpleClient
}
func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
v := &config.Validator{
Required: []string{
"token",
"email",
},
}
return v.Validate(c)
}
func (p *ResourceProvider) ValidateResource(
t string, c *terraform.ResourceConfig) ([]string, []error) {
return resourceMap.Validate(t, c)
}
func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
if _, err := config.Decode(&p.Config, c.Config); err != nil {
return err
}
log.Println("[INFO] Initializing DNSimple client")
var err error
p.client, err = p.Config.Client()
if err != nil {
return err
}
return nil
}
func (p *ResourceProvider) Apply(
s *terraform.ResourceState,
d *terraform.ResourceDiff) (*terraform.ResourceState, error) {
return resourceMap.Apply(s, d, p)
}
func (p *ResourceProvider) Diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) {
return resourceMap.Diff(s, c, p)
}
func (p *ResourceProvider) Refresh(
s *terraform.ResourceState) (*terraform.ResourceState, error) {
return resourceMap.Refresh(s, p)
}
func (p *ResourceProvider) Resources() []terraform.ResourceType {
return resourceMap.Resources()
}

View File

@ -0,0 +1,76 @@
package dnsimple
import (
"os"
"reflect"
"testing"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *ResourceProvider
func init() {
testAccProvider = new(ResourceProvider)
testAccProviders = map[string]terraform.ResourceProvider{
"dnsimple": testAccProvider,
}
}
func TestResourceProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = new(ResourceProvider)
}
func TestResourceProvider_Configure(t *testing.T) {
rp := new(ResourceProvider)
var expectedToken string
var expectedEmail string
if v := os.Getenv("DNSIMPLE_EMAIL"); v != "" {
expectedEmail = v
} else {
expectedEmail = "foo"
}
if v := os.Getenv("DNSIMPLE_TOKEN"); v != "" {
expectedToken = v
} else {
expectedToken = "foo"
}
raw := map[string]interface{}{
"token": expectedToken,
"email": expectedEmail,
}
rawConfig, err := config.NewRawConfig(raw)
if err != nil {
t.Fatalf("err: %s", err)
}
err = rp.Configure(terraform.NewResourceConfig(rawConfig))
if err != nil {
t.Fatalf("err: %s", err)
}
expected := Config{
Token: expectedToken,
Email: expectedEmail,
}
if !reflect.DeepEqual(rp.Config, expected) {
t.Fatalf("bad: %#v", rp.Config)
}
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("DNSIMPLE_EMAIL"); v == "" {
t.Fatal("DNSIMPLE_EMAIL must be set for acceptance tests")
}
if v := os.Getenv("DNSIMPLE_TOKEN"); v == "" {
t.Fatal("DNSIMPLE_TOKEN must be set for acceptance tests")
}
}

View File

@ -0,0 +1,24 @@
package dnsimple
import (
"github.com/hashicorp/terraform/helper/resource"
)
// resourceMap is the mapping of resources we support to their basic
// operations. This makes it easy to implement new resource types.
var resourceMap *resource.Map
func init() {
resourceMap = &resource.Map{
Mapping: map[string]resource.Resource{
"dnsimple_record": resource.Resource{
ConfigValidator: resource_dnsimple_record_validation(),
Create: resource_dnsimple_record_create,
Destroy: resource_dnsimple_record_destroy,
Diff: resource_dnsimple_record_diff,
Update: resource_dnsimple_record_update,
Refresh: resource_dnsimple_record_refresh,
},
},
}
}

View File

@ -0,0 +1,186 @@
package heroku
import (
"fmt"
"log"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/flatmap"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/diff"
"github.com/hashicorp/terraform/terraform"
)
func resource_heroku_addon_create(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
// Merge the diff into the state so that we have all the attributes
// properly.
rs := s.MergeDiff(d)
app := rs.Attributes["app"]
plan := rs.Attributes["plan"]
opts := heroku.AddonCreateOpts{}
if attr, ok := rs.Attributes["config.#"]; ok && attr == "1" {
vs := flatmap.Expand(
rs.Attributes, "config").([]interface{})
config := make(map[string]string)
for k, v := range vs[0].(map[string]interface{}) {
config[k] = v.(string)
}
opts.Config = &config
}
log.Printf("[DEBUG] Addon create configuration: %#v, %#v, %#v", app, plan, opts)
a, err := client.AddonCreate(app, plan, &opts)
if err != nil {
return s, err
}
rs.ID = a.Id
log.Printf("[INFO] Addon ID: %s", rs.ID)
addon, err := resource_heroku_addon_retrieve(app, rs.ID, client)
if err != nil {
return rs, err
}
return resource_heroku_addon_update_state(rs, addon)
}
func resource_heroku_addon_update(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
rs := s.MergeDiff(d)
app := rs.Attributes["app"]
if attr, ok := d.Attributes["plan"]; ok {
ad, err := client.AddonUpdate(
app, rs.ID,
attr.New)
if err != nil {
return s, err
}
// Store the new ID
rs.ID = ad.Id
}
addon, err := resource_heroku_addon_retrieve(app, rs.ID, client)
if err != nil {
return rs, err
}
return resource_heroku_addon_update_state(rs, addon)
}
func resource_heroku_addon_destroy(
s *terraform.ResourceState,
meta interface{}) error {
p := meta.(*ResourceProvider)
client := p.client
log.Printf("[INFO] Deleting Addon: %s", s.ID)
// Destroy the app
err := client.AddonDelete(s.Attributes["app"], s.ID)
if err != nil {
return fmt.Errorf("Error deleting addon: %s", err)
}
return nil
}
func resource_heroku_addon_refresh(
s *terraform.ResourceState,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
app, err := resource_heroku_addon_retrieve(s.Attributes["app"], s.ID, client)
if err != nil {
return nil, err
}
return resource_heroku_addon_update_state(s, app)
}
func resource_heroku_addon_diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig,
meta interface{}) (*terraform.ResourceDiff, error) {
b := &diff.ResourceBuilder{
Attrs: map[string]diff.AttrType{
"app": diff.AttrTypeCreate,
"plan": diff.AttrTypeUpdate,
"config": diff.AttrTypeCreate,
},
ComputedAttrs: []string{
"provider_id",
"config_vars",
},
}
return b.Diff(s, c)
}
func resource_heroku_addon_update_state(
s *terraform.ResourceState,
addon *heroku.Addon) (*terraform.ResourceState, error) {
s.Attributes["name"] = addon.Name
s.Attributes["plan"] = addon.Plan.Name
s.Attributes["provider_id"] = addon.ProviderId
toFlatten := make(map[string]interface{})
if len(addon.ConfigVars) > 0 {
toFlatten["config_vars"] = addon.ConfigVars
}
for k, v := range flatmap.Flatten(toFlatten) {
s.Attributes[k] = v
}
return s, nil
}
func resource_heroku_addon_retrieve(app string, id string, client *heroku.Client) (*heroku.Addon, error) {
addon, err := client.AddonInfo(app, id)
if err != nil {
return nil, fmt.Errorf("Error retrieving addon: %s", err)
}
return addon, nil
}
func resource_heroku_addon_validation() *config.Validator {
return &config.Validator{
Required: []string{
"app",
"plan",
},
Optional: []string{
"config.*",
},
}
}

View File

@ -0,0 +1,107 @@
package heroku
import (
"fmt"
"testing"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccHerokuAddon_Basic(t *testing.T) {
var addon heroku.Addon
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckHerokuAddonDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckHerokuAddonConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckHerokuAddonExists("heroku_addon.foobar", &addon),
testAccCheckHerokuAddonAttributes(&addon),
resource.TestCheckResourceAttr(
"heroku_addon.foobar", "config.0.url", "http://google.com"),
resource.TestCheckResourceAttr(
"heroku_addon.foobar", "app", "terraform-test-app"),
resource.TestCheckResourceAttr(
"heroku_addon.foobar", "plan", "deployhooks:http"),
),
},
},
})
}
func testAccCheckHerokuAddonDestroy(s *terraform.State) error {
client := testAccProvider.client
for _, rs := range s.Resources {
if rs.Type != "heroku_addon" {
continue
}
_, err := client.AddonInfo(rs.Attributes["app"], rs.ID)
if err == nil {
return fmt.Errorf("Addon still exists")
}
}
return nil
}
func testAccCheckHerokuAddonAttributes(addon *heroku.Addon) resource.TestCheckFunc {
return func(s *terraform.State) error {
if addon.Plan.Name != "deployhooks:http" {
return fmt.Errorf("Bad plan: %s", addon.Plan)
}
return nil
}
}
func testAccCheckHerokuAddonExists(n string, addon *heroku.Addon) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.ID == "" {
return fmt.Errorf("No Addon ID is set")
}
client := testAccProvider.client
foundAddon, err := client.AddonInfo(rs.Attributes["app"], rs.ID)
if err != nil {
return err
}
if foundAddon.Id != rs.ID {
return fmt.Errorf("Addon not found")
}
*addon = *foundAddon
return nil
}
}
const testAccCheckHerokuAddonConfig_basic = `
resource "heroku_app" "foobar" {
name = "terraform-test-app"
}
resource "heroku_addon" "foobar" {
app = "${heroku_app.foobar.name}"
plan = "deployhooks:http"
config {
url = "http://google.com"
}
}`

View File

@ -4,12 +4,45 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/flatmap"
"github.com/hashicorp/terraform/helper/config" "github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/diff" "github.com/hashicorp/terraform/helper/diff"
"github.com/hashicorp/terraform/helper/multierror"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/bgentry/heroku-go"
) )
// type application is used to store all the details of a heroku app
type application struct {
Id string // Id of the resource
App *heroku.App // The heroku application
Client *heroku.Client // Client to interact with the heroku API
Vars map[string]string // The vars on the application
}
// Updates the application to have the latest from remote
func (a *application) Update() error {
var errs []error
var err error
a.App, err = a.Client.AppInfo(a.Id)
if err != nil {
errs = append(errs, err)
}
a.Vars, err = retrieve_config_vars(a.Id, a.Client)
if err != nil {
errs = append(errs, err)
}
if len(errs) > 0 {
return &multierror.Error{Errors: errs}
}
return nil
}
func resource_heroku_app_create( func resource_heroku_app_create(
s *terraform.ResourceState, s *terraform.ResourceState,
d *terraform.ResourceDiff, d *terraform.ResourceDiff,
@ -38,15 +71,29 @@ func resource_heroku_app_create(
log.Printf("[DEBUG] App create configuration: %#v", opts) log.Printf("[DEBUG] App create configuration: %#v", opts)
app, err := client.AppCreate(&opts) a, err := client.AppCreate(&opts)
if err != nil { if err != nil {
return s, err return s, err
} }
rs.ID = app.Name rs.ID = a.Name
log.Printf("[INFO] App ID: %s", rs.ID) log.Printf("[INFO] App ID: %s", rs.ID)
if attr, ok := rs.Attributes["config_vars.#"]; ok && attr == "1" {
vs := flatmap.Expand(
rs.Attributes, "config_vars").([]interface{})
err = update_config_vars(rs.ID, vs, client)
if err != nil {
return rs, err
}
}
app, err := resource_heroku_app_retrieve(rs.ID, client)
if err != nil {
return rs, err
}
return resource_heroku_app_update_state(rs, app) return resource_heroku_app_update_state(rs, app)
} }
@ -54,10 +101,52 @@ func resource_heroku_app_update(
s *terraform.ResourceState, s *terraform.ResourceState,
d *terraform.ResourceDiff, d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) { meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
rs := s.MergeDiff(d)
panic("does not update") if attr, ok := d.Attributes["name"]; ok {
opts := heroku.AppUpdateOpts{
Name: &attr.New,
}
return nil, nil renamedApp, err := client.AppUpdate(rs.ID, &opts)
if err != nil {
return s, err
}
// Store the new ID
rs.ID = renamedApp.Name
}
attr, ok := s.Attributes["config_vars.#"]
// If the config var block was removed, nuke all config vars
if ok && attr == "1" {
vs := flatmap.Expand(
rs.Attributes, "config_vars").([]interface{})
err := update_config_vars(rs.ID, vs, client)
if err != nil {
return rs, err
}
} else if ok && attr == "0" {
log.Println("[INFO] Config vars removed, removing all vars")
err := update_config_vars(rs.ID, make([]interface{}, 0), client)
if err != nil {
return rs, err
}
}
app, err := resource_heroku_app_retrieve(rs.ID, client)
if err != nil {
return rs, err
}
return resource_heroku_app_update_state(rs, app)
} }
func resource_heroku_app_destroy( func resource_heroku_app_destroy(
@ -99,9 +188,10 @@ func resource_heroku_app_diff(
b := &diff.ResourceBuilder{ b := &diff.ResourceBuilder{
Attrs: map[string]diff.AttrType{ Attrs: map[string]diff.AttrType{
"name": diff.AttrTypeCreate, "name": diff.AttrTypeUpdate,
"region": diff.AttrTypeUpdate, "region": diff.AttrTypeUpdate,
"stack": diff.AttrTypeCreate, "stack": diff.AttrTypeCreate,
"config_vars": diff.AttrTypeUpdate,
}, },
ComputedAttrs: []string{ ComputedAttrs: []string{
@ -111,6 +201,7 @@ func resource_heroku_app_diff(
"git_url", "git_url",
"web_url", "web_url",
"id", "id",
"config_vars",
}, },
} }
@ -119,26 +210,41 @@ func resource_heroku_app_diff(
func resource_heroku_app_update_state( func resource_heroku_app_update_state(
s *terraform.ResourceState, s *terraform.ResourceState,
app *heroku.App) (*terraform.ResourceState, error) { app *application) (*terraform.ResourceState, error) {
s.Attributes["name"] = app.Name s.Attributes["name"] = app.App.Name
s.Attributes["stack"] = app.Stack.Name s.Attributes["stack"] = app.App.Stack.Name
s.Attributes["region"] = app.Region.Name s.Attributes["region"] = app.App.Region.Name
s.Attributes["git_url"] = app.GitURL s.Attributes["git_url"] = app.App.GitURL
s.Attributes["web_url"] = app.WebURL s.Attributes["web_url"] = app.App.WebURL
s.Attributes["id"] = app.Id
// We know that the hostname on heroku will be the name+herokuapp.com
// You need this to do things like create DNS CNAME records
s.Attributes["heroku_hostname"] = fmt.Sprintf("%s.herokuapp.com", app.App.Name)
toFlatten := make(map[string]interface{})
if len(app.Vars) > 0 {
toFlatten["config_vars"] = []map[string]string{app.Vars}
}
for k, v := range flatmap.Flatten(toFlatten) {
s.Attributes[k] = v
}
return s, nil return s, nil
} }
func resource_heroku_app_retrieve(id string, client *heroku.Client) (*heroku.App, error) { func resource_heroku_app_retrieve(id string, client *heroku.Client) (*application, error) {
app, err := client.AppInfo(id) app := application{Id: id, Client: client}
err := app.Update()
if err != nil { if err != nil {
return nil, fmt.Errorf("Error retrieving app: %s", err) return nil, fmt.Errorf("Error retrieving app: %s", err)
} }
return app, nil return &app, nil
} }
func resource_heroku_app_validation() *config.Validator { func resource_heroku_app_validation() *config.Validator {
@ -148,6 +254,38 @@ func resource_heroku_app_validation() *config.Validator {
"name", "name",
"region", "region",
"stack", "stack",
"config_vars.*",
}, },
} }
} }
func retrieve_config_vars(id string, client *heroku.Client) (map[string]string, error) {
vars, err := client.ConfigVarInfo(id)
if err != nil {
return nil, err
}
return vars, nil
}
// Updates the config vars for from an expanded (prior to assertion)
// []map[string]string config
func update_config_vars(id string, vs []interface{}, client *heroku.Client) error {
vars := make(map[string]*string)
for k, v := range vs[0].(map[string]interface{}) {
val := v.(string)
vars[k] = &val
}
log.Printf("[INFO] Updating config vars: *%#v", vars)
_, err := client.ConfigVarUpdate(id, vars)
if err != nil {
return fmt.Errorf("Error updating config vars: %s", err)
}
return nil
}

View File

@ -24,6 +24,78 @@ func TestAccHerokuApp_Basic(t *testing.T) {
testAccCheckHerokuAppAttributes(&app), testAccCheckHerokuAppAttributes(&app),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"heroku_app.foobar", "name", "terraform-test-app"), "heroku_app.foobar", "name", "terraform-test-app"),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "config_vars.0.FOO", "bar"),
),
},
},
})
}
func TestAccHerokuApp_NameChange(t *testing.T) {
var app heroku.App
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckHerokuAppDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckHerokuAppConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckHerokuAppExists("heroku_app.foobar", &app),
testAccCheckHerokuAppAttributes(&app),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "name", "terraform-test-app"),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "config_vars.0.FOO", "bar"),
),
},
resource.TestStep{
Config: testAccCheckHerokuAppConfig_updated,
Check: resource.ComposeTestCheckFunc(
testAccCheckHerokuAppExists("heroku_app.foobar", &app),
testAccCheckHerokuAppAttributesUpdated(&app),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "name", "terraform-test-renamed"),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "config_vars.0.FOO", "bing"),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "config_vars.0.BAZ", "bar"),
),
},
},
})
}
func TestAccHerokuApp_NukeVars(t *testing.T) {
var app heroku.App
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckHerokuAppDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckHerokuAppConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckHerokuAppExists("heroku_app.foobar", &app),
testAccCheckHerokuAppAttributes(&app),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "name", "terraform-test-app"),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "config_vars.0.FOO", "bar"),
),
},
resource.TestStep{
Config: testAccCheckHerokuAppConfig_no_vars,
Check: resource.ComposeTestCheckFunc(
testAccCheckHerokuAppExists("heroku_app.foobar", &app),
testAccCheckHerokuAppAttributesNoVars(&app),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "name", "terraform-test-app"),
resource.TestCheckResourceAttr(
"heroku_app.foobar", "config_vars.0.FOO", ""),
), ),
}, },
}, },
@ -50,8 +122,76 @@ func testAccCheckHerokuAppDestroy(s *terraform.State) error {
func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc { func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
client := testAccProvider.client
// check attrs if app.Region.Name != "us" {
return fmt.Errorf("Bad region: %s", app.Region.Name)
}
if app.Stack.Name != "cedar" {
return fmt.Errorf("Bad stack: %s", app.Stack.Name)
}
if app.Name != "terraform-test-app" {
return fmt.Errorf("Bad name: %s", app.Name)
}
vars, err := client.ConfigVarInfo(app.Name)
if err != nil {
return err
}
if vars["FOO"] != "bar" {
return fmt.Errorf("Bad config vars: %v", vars)
}
return nil
}
}
func testAccCheckHerokuAppAttributesUpdated(app *heroku.App) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.client
if app.Name != "terraform-test-renamed" {
return fmt.Errorf("Bad name: %s", app.Name)
}
vars, err := client.ConfigVarInfo(app.Name)
if err != nil {
return err
}
// Make sure we kept the old one
if vars["FOO"] != "bing" {
return fmt.Errorf("Bad config vars: %v", vars)
}
if vars["BAZ"] != "bar" {
return fmt.Errorf("Bad config vars: %v", vars)
}
return nil
}
}
func testAccCheckHerokuAppAttributesNoVars(app *heroku.App) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.client
if app.Name != "terraform-test-app" {
return fmt.Errorf("Bad name: %s", app.Name)
}
vars, err := client.ConfigVarInfo(app.Name)
if err != nil {
return err
}
if len(vars) != 0 {
return fmt.Errorf("vars exist: %v", vars)
}
return nil return nil
} }
@ -60,7 +200,7 @@ func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc {
func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFunc { func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
rs, ok := s.Resources[n] rs, ok := s.Resources[n]
fmt.Printf("resources %#v", s.Resources)
if !ok { if !ok {
return fmt.Errorf("Not found: %s", n) return fmt.Errorf("Not found: %s", n)
} }
@ -81,7 +221,7 @@ func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFu
return fmt.Errorf("App not found") return fmt.Errorf("App not found")
} }
app = foundApp *app = *foundApp
return nil return nil
} }
@ -89,5 +229,24 @@ func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFu
const testAccCheckHerokuAppConfig_basic = ` const testAccCheckHerokuAppConfig_basic = `
resource "heroku_app" "foobar" { resource "heroku_app" "foobar" {
name = "terraform-test-app" name = "terraform-test-app"
config_vars {
FOO = "bar"
}
}`
const testAccCheckHerokuAppConfig_updated = `
resource "heroku_app" "foobar" {
name = "terraform-test-renamed"
config_vars {
FOO = "bing"
BAZ = "bar"
}
}`
const testAccCheckHerokuAppConfig_no_vars = `
resource "heroku_app" "foobar" {
name = "terraform-test-app"
}` }`

View File

@ -0,0 +1,126 @@
package heroku
import (
"fmt"
"log"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/diff"
"github.com/hashicorp/terraform/terraform"
)
func resource_heroku_domain_create(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
// Merge the diff into the state so that we have all the attributes
// properly.
rs := s.MergeDiff(d)
app := rs.Attributes["app"]
hostname := rs.Attributes["hostname"]
log.Printf("[DEBUG] Domain create configuration: %#v, %#v", app, hostname)
do, err := client.DomainCreate(app, hostname)
if err != nil {
return s, err
}
rs.ID = do.Id
rs.Attributes["hostname"] = do.Hostname
rs.Attributes["cname"] = fmt.Sprintf("%s.herokuapp.com", app)
log.Printf("[INFO] Domain ID: %s", rs.ID)
return rs, nil
}
func resource_heroku_domain_update(
s *terraform.ResourceState,
d *terraform.ResourceDiff,
meta interface{}) (*terraform.ResourceState, error) {
panic("Cannot update domain")
return nil, nil
}
func resource_heroku_domain_destroy(
s *terraform.ResourceState,
meta interface{}) error {
p := meta.(*ResourceProvider)
client := p.client
log.Printf("[INFO] Deleting Domain: %s", s.ID)
// Destroy the app
err := client.DomainDelete(s.Attributes["app"], s.ID)
if err != nil {
return fmt.Errorf("Error deleting domain: %s", err)
}
return nil
}
func resource_heroku_domain_refresh(
s *terraform.ResourceState,
meta interface{}) (*terraform.ResourceState, error) {
p := meta.(*ResourceProvider)
client := p.client
domain, err := resource_heroku_domain_retrieve(s.Attributes["app"], s.ID, client)
if err != nil {
return nil, err
}
s.Attributes["hostname"] = domain.Hostname
s.Attributes["cname"] = fmt.Sprintf("%s.herokuapp.com", s.Attributes["app"])
return s, nil
}
func resource_heroku_domain_diff(
s *terraform.ResourceState,
c *terraform.ResourceConfig,
meta interface{}) (*terraform.ResourceDiff, error) {
b := &diff.ResourceBuilder{
Attrs: map[string]diff.AttrType{
"hostname": diff.AttrTypeCreate,
"app": diff.AttrTypeCreate,
},
ComputedAttrs: []string{
"cname",
},
}
return b.Diff(s, c)
}
func resource_heroku_domain_retrieve(app string, id string, client *heroku.Client) (*heroku.Domain, error) {
domain, err := client.DomainInfo(app, id)
if err != nil {
return nil, fmt.Errorf("Error retrieving domain: %s", err)
}
return domain, nil
}
func resource_heroku_domain_validation() *config.Validator {
return &config.Validator{
Required: []string{
"hostname",
"app",
},
Optional: []string{},
}
}

View File

@ -0,0 +1,104 @@
package heroku
import (
"fmt"
"testing"
"github.com/bgentry/heroku-go"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccHerokuDomain_Basic(t *testing.T) {
var domain heroku.Domain
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckHerokuDomainDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckHerokuDomainConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckHerokuDomainExists("heroku_domain.foobar", &domain),
testAccCheckHerokuDomainAttributes(&domain),
resource.TestCheckResourceAttr(
"heroku_domain.foobar", "hostname", "terraform.example.com"),
resource.TestCheckResourceAttr(
"heroku_domain.foobar", "app", "terraform-test-app"),
resource.TestCheckResourceAttr(
"heroku_domain.foobar", "cname", "terraform-test-app.herokuapp.com"),
),
},
},
})
}
func testAccCheckHerokuDomainDestroy(s *terraform.State) error {
client := testAccProvider.client
for _, rs := range s.Resources {
if rs.Type != "heroku_domain" {
continue
}
_, err := client.DomainInfo(rs.Attributes["app"], rs.ID)
if err == nil {
return fmt.Errorf("Domain still exists")
}
}
return nil
}
func testAccCheckHerokuDomainAttributes(Domain *heroku.Domain) resource.TestCheckFunc {
return func(s *terraform.State) error {
if Domain.Hostname != "terraform.example.com" {
return fmt.Errorf("Bad hostname: %s", Domain.Hostname)
}
return nil
}
}
func testAccCheckHerokuDomainExists(n string, Domain *heroku.Domain) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.ID == "" {
return fmt.Errorf("No Domain ID is set")
}
client := testAccProvider.client
foundDomain, err := client.DomainInfo(rs.Attributes["app"], rs.ID)
if err != nil {
return err
}
if foundDomain.Id != rs.ID {
return fmt.Errorf("Domain not found")
}
*Domain = *foundDomain
return nil
}
}
const testAccCheckHerokuDomainConfig_basic = `
resource "heroku_app" "foobar" {
name = "terraform-test-app"
}
resource "heroku_domain" "foobar" {
app = "${heroku_app.foobar.name}"
hostname = "terraform.example.com"
}`

View File

@ -11,6 +11,15 @@ var resourceMap *resource.Map
func init() { func init() {
resourceMap = &resource.Map{ resourceMap = &resource.Map{
Mapping: map[string]resource.Resource{ Mapping: map[string]resource.Resource{
"heroku_addon": resource.Resource{
ConfigValidator: resource_heroku_addon_validation(),
Create: resource_heroku_addon_create,
Destroy: resource_heroku_addon_destroy,
Diff: resource_heroku_addon_diff,
Refresh: resource_heroku_addon_refresh,
Update: resource_heroku_addon_update,
},
"heroku_app": resource.Resource{ "heroku_app": resource.Resource{
ConfigValidator: resource_heroku_app_validation(), ConfigValidator: resource_heroku_app_validation(),
Create: resource_heroku_app_create, Create: resource_heroku_app_create,
@ -19,6 +28,14 @@ func init() {
Refresh: resource_heroku_app_refresh, Refresh: resource_heroku_app_refresh,
Update: resource_heroku_app_update, Update: resource_heroku_app_update,
}, },
"heroku_domain": resource.Resource{
ConfigValidator: resource_heroku_domain_validation(),
Create: resource_heroku_domain_create,
Destroy: resource_heroku_domain_destroy,
Diff: resource_heroku_domain_diff,
Refresh: resource_heroku_domain_refresh,
},
}, },
} }
} }

View File

@ -55,7 +55,7 @@ func (v *FlagVarFile) Set(raw string) error {
return nil return nil
} }
const libuclParseFlags = libucl.ParserKeyLowercase const libuclParseFlags = libucl.ParserNoTime
func loadVarFile(path string) (map[string]string, error) { func loadVarFile(path string) (map[string]string, error) {
var obj *libucl.Object var obj *libucl.Object

View File

@ -29,13 +29,14 @@ var ContextOpts terraform.ContextOpts
// Put the parse flags we use for libucl in a constant so we can get // Put the parse flags we use for libucl in a constant so we can get
// equally behaving parsing everywhere. // equally behaving parsing everywhere.
const libuclParseFlags = libucl.ParserKeyLowercase const libuclParseFlags = libucl.ParserNoTime
func init() { func init() {
BuiltinConfig.Providers = map[string]string{ BuiltinConfig.Providers = map[string]string{
"aws": "terraform-provider-aws", "aws": "terraform-provider-aws",
"digitalocean": "terraform-provider-digitalocean", "digitalocean": "terraform-provider-digitalocean",
"heroku": "terraform-provider-heroku", "heroku": "terraform-provider-heroku",
"dnsimple": "terraform-provider-dnsimple",
} }
BuiltinConfig.Provisioners = map[string]string{ BuiltinConfig.Provisioners = map[string]string{
"local-exec": "terraform-provisioner-local-exec", "local-exec": "terraform-provisioner-local-exec",

View File

@ -9,7 +9,7 @@ import (
// Put the parse flags we use for libucl in a constant so we can get // Put the parse flags we use for libucl in a constant so we can get
// equally behaving parsing everywhere. // equally behaving parsing everywhere.
const libuclParseFlags = libucl.ParserKeyLowercase const libuclParseFlags = libucl.ParserNoTime
// libuclConfigurable is an implementation of configurable that knows // libuclConfigurable is an implementation of configurable that knows
// how to turn libucl configuration into a *Config object. // how to turn libucl configuration into a *Config object.

View File

@ -485,6 +485,7 @@ do
const basicResourcesStr = ` const basicResourcesStr = `
aws_instance[db] (x1) aws_instance[db] (x1)
VPC
security_groups security_groups
dependsOn dependsOn
aws_instance.web aws_instance.web

View File

@ -31,6 +31,7 @@ resource aws_instance "web" {
resource "aws_instance" "db" { resource "aws_instance" "db" {
security_groups = "${aws_security_group.firewall.*.id}" security_groups = "${aws_security_group.firewall.*.id}"
VPC = "foo"
depends_on = ["aws_instance.web"] depends_on = ["aws_instance.web"]
} }

View File

@ -21,6 +21,7 @@
"aws_instance": { "aws_instance": {
"db": { "db": {
"security_groups": ["${aws_security_group.firewall.*.id}"], "security_groups": ["${aws_security_group.firewall.*.id}"],
"VPC": "foo",
"depends_on": ["aws_instance.web"] "depends_on": ["aws_instance.web"]
}, },

View File

@ -1,93 +1,80 @@
--- ---
layout: "inner"
page_title: "Community" page_title: "Community"
--- ---
<div class="container"> <h1>Community</h1>
<div class="col-md-8 col-md-offset-2">
<div class="bs-docs-section">
<h1>Community</h1>
<p>
Terraform is a new project with a growing community. Despite this,
there are active, dedicated users willing to help you through various
mediums.
</p>
<p>
<strong>IRC:</strong> <code>#terraform</code> on Freenode
</p>
<p>
<strong>Mailing list:</strong>
<a href="https://groups.google.com/group/terraform-tool">Terraform Google Group</a>
</p>
<p>
<strong>Bug Tracker:</strong>
<a href="https://github.com/hashicorp/terraform/issues">Issue tracker
on GitHub</a>. Please only use this for reporting bugs. Do not ask
for general help here. Use IRC or the mailing list for that.
<h1>People</h1>
<p>
The following people are some of the faces behind Terraform. They each
contribute to Terraform in some core way. Over time, faces may appear and
disappear from this list as contributors come and go.
</p>
<div class="people">
<div class="person">
<img class="pull-left" src="http://www.gravatar.com/avatar/54079122b67de9677c1f93933ce8b63a.png?s=125">
<div class="bio">
<h3>Mitchell Hashimoto (<a href="https://github.com/mitchellh">@mitchellh</a>)</h3>
<p> <p>
Terraform is a new project with a growing community. Despite this, Mitchell Hashimoto is the creator of Terraform and works on all
there are active, dedicated users willing to help you through various layers of Terraform from the core to providers. In addition to Terraform,
mediums. Mitchell is the creator of
<a href="http://www.vagrantup.com">Vagrant</a>,
<a href="http://www.packer.io">Packer</a>, and
<a href="http://www.consul.io">Consul</a>.
</p> </p>
<p>
<strong>IRC:</strong> <code>#terraform</code> on Freenode
</p>
<p>
<strong>Mailing list:</strong>
<a href="https://groups.google.com/group/terraform-tool">Terraform Google Group</a>
</p>
<p>
<strong>Bug Tracker:</strong>
<a href="https://github.com/hashicorp/terraform/issues">Issue tracker
on GitHub</a>. Please only use this for reporting bugs. Do not ask
for general help here. Use IRC or the mailing list for that.
<h1>People</h1>
<p>
The following people are some of the faces behind Terraform. They each
contribute to Terraform in some core way. Over time, faces may appear and
disappear from this list as contributors come and go.
</p>
<div class="people">
<div class="person">
<img class="pull-left" src="http://www.gravatar.com/avatar/11ba9630c9136eef9a70d26473d355d5.png?s=125">
<div class="bio">
<h3>Armon Dadgar (<a href="https://github.com/armon">@armon</a>)</h3>
<p>
Armon Dadgar is the creator of Terraform. He researched and developed
most of the internals of how Terraform works, including the
gossip layer, leader election, etc. Armon is also the creator of
<a href="https://github.com/hashicorp/serf">Serf</a>,
<a href="https://github.com/armon/statsite">Statsite</a>, and
<a href="https://github.com/armon/bloomd">Bloomd</a>.
</div>
</div>
<div class="person">
<img class="pull-left" src="http://www.gravatar.com/avatar/54079122b67de9677c1f93933ce8b63a.png?s=125">
<div class="bio">
<h3>Mitchell Hashimoto (<a href="https://github.com/mitchellh">@mitchellh</a>)</h3>
<p>
Mitchell Hashimoto is a co-creator of Terraform. He primarily took
a management role in the creation of Terraform, guiding product
and user experience decisions on top of Armon's technical decisions.
Mitchell Hashimoto is also the creator of
<a href="http://www.vagrantup.com">Vagrant</a>,
<a href="http://www.packer.io">Packer</a>, and
<a href="http://www.serfdom.io">Serf</a>.
</p>
</div>
</div>
<div class="person">
<img class="pull-left" src="http://www.gravatar.com/avatar/2acc31dd6370a54b18f6755cd0710ce6.png?s=125">
<div class="bio">
<h3>Jack Pearkes (<a href="https://github.com/pearkes">@pearkes</a>)</h3>
<p>
Jack Pearkes created and maintains the Terraform web UI.
He is also a core committer to
<a href="http://www.packer.io">Packer</a> and maintains
many successful
<a href="https://github.com/pearkes">open source projects</a>
while also being an employee of
<a href="http://www.hashicorp.com">HashiCorp</a>.
</p>
</div>
</div>
<div class="person">
<img class="pull-left" src="//www.gravatar.com/avatar/1e87e6016a7c4f4ecbd2517d84058467.png?s=125">
<div class="bio">
<h3>William Tisäter (<a href="https://github.com/tiwilliam">@tiwilliam</a>)</h3>
<p>William Tisäter is a Terraform core committer. He is also maintainer of <a href="https://pypi.python.org/pypi/pygeoip">pygeoip</a> and build things daily at <a href="https://tictail.com">Tictail</a>.</p>
</div>
</div>
<div class="clearfix"></div>
</div>
</div> </div>
</div> </div>
<div class="person">
<img class="pull-left" src="http://www.gravatar.com/avatar/11ba9630c9136eef9a70d26473d355d5.png?s=125">
<div class="bio">
<h3>Armon Dadgar (<a href="https://github.com/armon">@armon</a>)</h3>
<p>
Armon Dadgar is a creator of Terraform. He created valuable sections
of the core and helps maintain providers as well. Armon is also the
creator of
<a href="http://www.consul.io">Consul</a>,
<a href="http://www.serfdom.io">Serf</a>,
<a href="https://github.com/armon/statsite">Statsite</a>, and
<a href="https://github.com/armon/bloomd">Bloomd</a>.
</div>
</div>
<div class="person">
<img class="pull-left" src="http://www.gravatar.com/avatar/2acc31dd6370a54b18f6755cd0710ce6.png?s=125">
<div class="bio">
<h3>Jack Pearkes (<a href="https://github.com/pearkes">@pearkes</a>)</h3>
<p>
Jack Pearkes is a creator of Terraform. He created and maintains
most of the providers and documentation.
He is also a core committer to
<a href="http://www.packer.io">Packer</a> and
<a href="http://www.consul.io">Consul</a>
while also being an employee of
<a href="http://www.hashicorp.com">HashiCorp</a>.
</p>
</div>
</div>
<div class="clearfix"></div>
</div> </div>

View File

@ -0,0 +1,43 @@
---
layout: "aws"
page_title: "Provider: AWS"
sidebar_current: "docs-aws-index"
---
# AWS Provider
The Amazon Web Services (AWS) provider is used to interact with the
many resources supported by AWS. The provider needs to be configured
with the proper credentials before it can be used.
Use the navigation to the left to read about the available resources.
## Example Usage
```
# Configure the AWS Provider
provider "aws" {
access_key = "${var.aws_access_key}"
secret_key = "${var.aws_secret_key}"
region = "us-east-1"
}
# Create a web server
resource "aws_instance" "web" {
...
}
```
## Argument Reference
The following arguments are supported:
* `access_key` - (Required) This is the AWS access key. It must be provided, but
it can also be sourced from the `AWS_ACCESS_KEY` environment variable.
* `secret_key` - (Required) This is the AWS secret key. It must be provided, but
it can also be sourced from the `AWS_SECRET_KEY` environment variable.
* `region` - (Required) This is the AWS region. It must be provided, but
it can also be sourced from the `AWS_REGION` environment variables.

View File

@ -0,0 +1,33 @@
---
layout: "aws"
page_title: "AWS: aws_eip"
sidebar_current: "docs-aws-resource-eip"
---
# aws\_eip
Provides an Elastic IP resource.
## Example Usage
```
resource "aws_eip" "lb" {
instance = "${aws_instance.web.instance_id}"
}
```
## Argument Reference
The following arguments are supported:
* `vpc` - (Optional) VPC ID
* `instance` - (Optional) EC2 instance ID.
## Attributes Reference
The following attributes are exported:
* `private_ip` - Contrains the private IP address (if in VPC).
* `public_ip` - Contains the public IP address.
* `instance` - Contains the ID of the instance attached ot.

View File

@ -0,0 +1,49 @@
---
layout: "aws"
page_title: "AWS: aws_instance"
sidebar_current: "docs-aws-resource-instance"
---
# aws\_instance
Provides an EC2 instance resource. This allows instances to be created, updated,
and deleted. Instances also support [provisioning](/docs/provisioners/index.html).
## Example Usage
```
# Create a new instance of the ami-1234 on an m1.small node
resource "aws_instance" "web" {
ami = "ami-1234"
instance_type = "m1.small"
}
```
## Argument Reference
The following arguments are supported:
* `ami` - (Required) The AMI to use for the instance.
* `availability_zone` - (Optional) The AZ to start the instance in.
* `instance_type` - (Required) The type of instance to start
* `key_name` - (Optional) The key name to use for the instance.
* `security_groups` - (Optional) A list of security group IDs to associate with.
* `subnet_id` - (Optional) The VPC Subnet ID to launch in.
* `source_dest_check` - (Optional) Controls if traffic is routed to the instance when
the destination address does not match the instance. Used for NAT or VPNs. Defaults false.
* `user_data` - (Optional) The user data to provide when launching the instance.
## Attributes Reference
The following attributes are exported:
* `id` - The instance ID.
* `availability_zone` - The availability zone of the instance.
* `key_name` - The key name of the instance
* `private_dns` - The Private DNS name of the instance
* `private_ip` - The private IP address.
* `public_dns` - The public DNS name of the instance
* `public_ip` - The public IP address.
* `security_groups` - The associated security groups.
* `subnet_id` - The VPC subnet ID.

View File

@ -0,0 +1,30 @@
---
layout: "aws"
page_title: "AWS: aws_internet_gateway"
sidebar_current: "docs-aws-resource-internet-gateway"
---
# aws\_internet\_gateway
Provides a resource to create a VPC Internet Gateway.
## Example Usage
```
resource "aws_internet_gateway" "gw" {
vpc_id = "${aws_vpc.main.id}"
}
```
## Argument Reference
The following arguments are supported:
* `vpc_id` - (Required) The VPC ID to create in.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the Internet Gateway.

View File

@ -0,0 +1,36 @@
---
layout: "aws"
page_title: "AWS: aws_launch_configuration"
sidebar_current: "docs-aws-resource-launch-config"
---
# aws\_launch\_configuration
Provides a resource to create a new launch configuration, used for autoscaling groups.
## Example Usage
```
resource "aws_launch_configuration" "as_conf" {
name = "web_config"
image_id = "ami-1234"
instance_type = "m1.small"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the launch configuration.
* `image_id` - (Required) The EC2 image ID to launch.
* `instance_type` - (Required) The size of instance to launch.
* `key_name` - (Optional) The key name that should be used for the instance.
* `security_groups` - (Optional) A list of associated security group IDS.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the launch configuration.

View File

@ -0,0 +1,36 @@
---
layout: "aws"
page_title: "AWS: aws_route53_record"
sidebar_current: "docs-aws-resource-route53-record"
---
# aws\_route53\_record
Provides a Route53 record resource.
## Example Usage
```
resource "aws_route53_record" "www" {
zone_id = "${aws_route53_zone.primary.zone_id}"
name = "www.example.com"
type = "A"
ttl = "300"
records = ["${aws_eip.lb.public_ip}"]
}
```
## Argument Reference
The following arguments are supported:
* `zone_id` - (Required) The ID of the hosted zone to contain this record.
* `name` - (Required) The name of the record.
* `type` - (Required) The record type.
* `ttl` - (Required) The TTL of the record.
* `records` - (Required) A string list of records.
## Attributes Reference
No attributes are exported.

View File

@ -0,0 +1,30 @@
---
layout: "aws"
page_title: "AWS: aws_route53_zone"
sidebar_current: "docs-aws-resource-route53-zone"
---
# aws\_route53\_zone
Provides a Route53 Hosted Zone resource.
## Example Usage
```
resource "aws_route53_zone" "primary" {
name = "example.com"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) This is the name of the hosted zone.
## Attributes Reference
The following attributes are exported:
* `zone_id` - The Hosted Zone ID. This can be referenced by zone records.

View File

@ -0,0 +1,40 @@
---
layout: "aws"
page_title: "AWS: aws_route_table"
sidebar_current: "docs-aws-resource-route-table|"
---
# aws\_route\_table
Provides a resource to create a VPC routing table.
## Example Usage
```
resource "aws_route_table" "r" {
vpc_id = "${aws_vpc.default.id}"
route {
cidr_block = "10.0.1.0/24"
}
}
```
## Argument Reference
The following arguments are supported:
* `vpc_id` - (Required) The ID of the routing table.
* `route` - (Optional) A list of route objects. Their keys are documented below.
Each route supports the following:
* `cidr_block` - (Required) The CIDR block of the route.
* `gateway_id` - (Optional) The Internet Gateway ID.
* `instance_id` - (Optional) The EC2 instance ID.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the routing table

View File

@ -0,0 +1,32 @@
---
layout: "aws"
page_title: "AWS: aws_route_table_association"
sidebar_current: "docs-aws-resource-route-table-assoc"
---
# aws\_route\_table\_association
Provides a resource to create an association between a subnet and routing table.
## Example Usage
```
resource "aws_route_table_association" "a" {
subnet_id = "${aws_subnet.foo.id}"
route_table_id = "${aws_route_table.bar.id}"
}
```
## Argument Reference
The following arguments are supported:
* `subnet_id` - (Required) The subnet ID to create an association.
* `route_table_id` - (Required) The ID of the routing table to associate with.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the association

View File

@ -0,0 +1,32 @@
---
layout: "aws"
page_title: "AWS: aws_s3_bucket"
sidebar_current: "docs-aws-resource-s3-bucket"
---
# aws\_s3\_bucket
Provides a S3 bucket resource.
## Example Usage
```
resource "aws_s3_bucket" "b" {
bucket = "my_tf_test_bucket"
acl = "private"
}
```
## Argument Reference
The following arguments are supported:
* `bucket` - (Required) The name of the bucket.
* `acl` - (Optional) The canned ACL to apply. Defaults to "private".
## Attributes Reference
The following attributes are exported:
* `id` - The name of the bucket

View File

@ -0,0 +1,54 @@
---
layout: "aws"
page_title: "AWS: aws_security_group"
sidebar_current: "docs-aws-resource-security-group"
---
# aws\_security\_group
Provides an security group resource.
## Example Usage
```
resource "aws_security_group" "allow_all" {
name = "allow_all"
ingress {
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the security group
* `ingress` - (Required) Can be specified multiple times for each
ingress rule. Each ingress block supports fields documented below.
* `description` - (Optional) The security group description.
* `vpc_id` - (Optional) The VPC ID.
* `owner_id` - (Optional) The AWS Owner ID.
The `ingress` block supports:
* `cidr_blocks` - (Optional) List of CIDR blocks. Cannot be used with `security_groups`.
* `from_port` - (Required) The start port.
* `protocol` - (Required) The protocol.
* `security_groups` - (Optional) List of security group IDs. Cannot be used with `cidr_blocks`.
* `to_port` - (Required) The end range port.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the security group
* `vpc_id` - The VPC ID.
* `owner_id` - The owner ID.
* `name` - The name of the security group
* `description` - The description of the security group
* `ingress` - The ingress rules. See above for more.

View File

@ -0,0 +1,36 @@
---
layout: "aws"
page_title: "AWS: aws_subnet"
sidebar_current: "docs-aws-resource-subnet"
---
# aws\_subnet
Provides an VPC subnet resource.
## Example Usage
```
resource "aws_subnet" "main" {
vpc_id = "${aws_vpc.main.id}"
cidr_block = "10.0.1.0/16"
}
```
## Argument Reference
The following arguments are supported:
* `availability_zone`- (Optional) The AZ for the subnet.
* `cidr_block` - (Required) The CIDR block for the subnet.
* `vpc_id` - (Required) The VPC ID.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the subnet
* `availability_zone`- The AZ for the subnet.
* `cidr_block` - The CIDR block for the subnet.
* `vpc_id` - The VPC ID.

View File

@ -0,0 +1,31 @@
---
layout: "aws"
page_title: "AWS: aws_vpc"
sidebar_current: "docs-aws-resource-vpc"
---
# aws\_vpc
Provides an VPC resource.
## Example Usage
```
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
```
## Argument Reference
The following arguments are supported:
* `cidr_block` - (Required) The CIDR block for the VPC.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the VPC
* `cidr_block` - The CIDR block of the VPC

View File

@ -0,0 +1,18 @@
---
layout: "docs"
page_title: "Providers"
sidebar_current: "docs-providers"
---
# Providers
Terraform is used to create, manage, and manipulate infrastructure resources.
Examples of resources include physical machines, VMs, network switches, containers,
etc. Almost any infrastructure noun can be represented as a resource in Terraform.
Terraform is agnostic to the underlying platforms by supporting providers. A provider
is responsible for understanding API interactions and exposing resources. Providers
generally are an IaaS (e.g. AWS, DigitalOcean, GCE), PaaS (e.g. Heroku, CloudFoundry),
or SaaS services (e.g. DNSimple, CloudFlare).
Use the navigation to the left to read about the available providers.

View File

@ -0,0 +1,53 @@
---
layout: "docs"
page_title: "Provisioner Connections"
sidebar_current: "docs-provisioners-connection"
---
# Provisioner Connections
Many provisioners require access to the remote resource. For example,
a provisioner may need to use ssh to connect to the resource.
Terraform uses a number of defaults when connecting to a resource, but these
can be overriden using `connection` block in either a `resource` or `provisioner`.
Any `connection` information provided in a `resource` will apply to all the
provisioners, but it can be scoped to a single provisioner as well. One use case
is to have an initial provisioner connect as root to setup user accounts, and have
subsequent provisioners connect as a user with more limited permissions.
## Example usage
```
# Copies the file as the root user using a password
provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
connection {
user = "root"
password = "${var.root_password}"
}
}
```
## Argument Reference
The following arugments are supported:
* `type` - The connection type that should be used. This defaults to "ssh". The type
of connection supported depends on the provisioner.
* `user` - The user that we should use for the connection. This defaults to "root".
* `password` - The password we should use for the connection.
* `key_file` - The SSH key to use for the connection. This takes preference over the
password if provided.
* `host` - The address of the resource to connect to. This is provided by the provider.
* `port` - The port to connect to. This defaults to 22.
* `timeout` - The timeout to wait for the conneciton to become available. This defaults
to 5 minutes. Should be provided as a string like "30s" or "5m".

View File

@ -0,0 +1,64 @@
---
layout: "docs"
page_title: "Provisioner: file"
sidebar_current: "docs-provisioners-file"
---
# File Provisioner
The `file` provisioner is used to copy files or directories from the machine
executing Terraform to the newly created resource. The `file` provisioner only
supports `ssh` type [connections](/docs/provisioners/connection.html).
## Example usage
```
resource "aws_instance" "web" {
...
# Copies the myapp.conf file to /etc/myapp.conf
provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
}
# Copies the configs.d folder to /etc/configs.d
provisioner "file" {
source = "conf/configs.d"
destination = "/etc"
}
}
```
## Argument Reference
The following arugments are supported:
* `source` - (Required) This is the source file or folder. It can be specified as relative
to the current working directory or as an absolute path.
* `destination` - (Required) This is the destination path. It must be specified as an
absolute path.
## Directory Uploads
The file provisioner is also able to upload a complete directory to the remote machine.
When uploading a directory, there are a few important things you should know.
First, the destination directory must already exist. If you need to create it,
use a remote-exec provisioner just prior to the file provisioner in order to create the directory.
Next, the existence of a trailing slash on the source path will determine whether the
directory name will be embedded within the destination, or whether the destination will
be created. An example explains this best:
If the source is `/foo` (no trailing slash), and the destination is `/tmp`, then the contents
of `/foo` on the local machine will be uploaded to `/tmp/foo` on the remote machine. The
`foo` directory on the remote machine will be created by Terraform.
If the source, however, is `/foo/` (a trailing slash is present), and the destination is
`/tmp`, then the contents of `/foo` will be uploaded directly into `/tmp` directly.
This behavior was adopted from the standard behavior of rsync. Note that under the covers,
rsync may or may not be used.

View File

@ -0,0 +1,15 @@
---
layout: "docs"
page_title: "Provisioners"
sidebar_current: "docs-provisioners"
---
# Provisioners
When a resource is initially created, provisioners can be executed to
initialize that resource. This can be used to add resources to an inventory
management system, run a configuration management tool, bootstrap the
resource into a cluster, etc.
Use the navigation to the left to read about the available provisioners.

View File

@ -0,0 +1,34 @@
---
layout: "docs"
page_title: "Provisioner: local-exec"
sidebar_current: "docs-provisioners-local"
---
# local-exec Provisioner
The `local-exec` provisioner invokes a local executable after a resource
is created. This invokes a process on the machine running Terraform, not on
the resource. See the `remote-exec` [provisioner](/docs/provisioners/remote-exec.html)
to run commands on the resource.
## Example usage
```
# Join the newly created machine to our Consul cluster
resource "aws_instance" "web" {
...
provisioner "local-exec" {
command = "consul join ${aws_instance.web.private_ip}"
}
}
```
## Argument Reference
The following arugments are supported:
* `command` - (Required) This is the command to execute. It can be provided
as a relative path to the current working directory or as an absolute path.
It is evaluated in a shell, and can use environment variables or Terraform
variables.

View File

@ -0,0 +1,45 @@
---
layout: "docs"
page_title: "Provisioner: remote-exec"
sidebar_current: "docs-provisioners-remote"
---
# remote-exec Provisioner
The `remote-exec` provisioner invokes a script on a remote resource after it
is created. This can be used to run a configuration management tool, bootstrap
into a cluster, etc. To invoke a local process, see the `local-exec`
[provisioner](/docs/provisioners/local-exec.html) instead. The `remote-exec`
provisioner only supports `ssh` type [connections](/docs/provisioners/connection.html).
## Example usage
```
# Run puppet and join our Consul cluster
resource "aws_instance" "web" {
...
provisioner "remote-exec" {
inline = [
"puppet apply",
"consul join ${aws_instance.web.private_ip",
]
}
}
```
## Argument Reference
The following arguments are supported:
* `inline` - This is a list of command strings. They are executed in the order
they are provided. This cannot be provided with `script` or `scripts`.
* `script` - This is a path (relative or absolute) to a local script that will
be copied to the remote resource and then executed. This cannot be provided
with `inline` or `scripts`.
* `scripts` - This is a list of paths (relative or absolute) to local scripts
that will be copied to the remote resource and then executed. They are executed
in the order they are provided. This cannot be provided with `inline` or `script`.

View File

@ -1,125 +0,0 @@
---
layout: "intro"
page_title: "Run the Agent"
sidebar_current: "gettingstarted-agent"
---
# Run the Terraform Agent
After Terraform is installed, the agent must be run. The agent can either run
in a server or client mode. Each datacenter must have at least one server,
although 3 or 5 is recommended. A single server deployment is _**highly**_ discouraged
as data loss is inevitable in a failure scenario. [This guide](/docs/guides/bootstrapping.html)
covers bootstrapping a new datacenter. All other agents run in client mode, which
is a very lightweight process that registers services, runs health checks,
and forwards queries to servers. The agent must be run for every node that
will be part of the cluster.
## Starting the Agent
For simplicity, we'll run a single Terraform agent in server mode right now:
```
$ terraform agent -server -bootstrap -data-dir /tmp/consul
==> WARNING: Bootstrap mode enabled! Do not enable unless necessary
==> WARNING: It is highly recommended to set GOMAXPROCS higher than 1
==> Starting Terraform agent...
==> Starting Terraform agent RPC...
==> Terraform agent running!
Node name: 'Armons-MacBook-Air'
Datacenter: 'dc1'
Server: true (bootstrap: true)
Client Addr: 127.0.0.1 (HTTP: 8500, DNS: 8600, RPC: 8400)
Cluster Addr: 10.1.10.38 (LAN: 8301, WAN: 8302)
==> Log data will now stream in as it occurs:
[INFO] serf: EventMemberJoin: Armons-MacBook-Air.local 10.1.10.38
[INFO] raft: Node at 10.1.10.38:8300 [Follower] entering Follower state
[INFO] terraform: adding server for datacenter: dc1, addr: 10.1.10.38:8300
[ERR] agent: failed to sync remote state: rpc error: No cluster leader
[WARN] raft: Heartbeat timeout reached, starting election
[INFO] raft: Node at 10.1.10.38:8300 [Candidate] entering Candidate state
[INFO] raft: Election won. Tally: 1
[INFO] raft: Node at 10.1.10.38:8300 [Leader] entering Leader state
[INFO] terraform: cluster leadership acquired
[INFO] terraform: New leader elected: Armons-MacBook-Air
[INFO] terraform: member 'Armons-MacBook-Air' joined, marking health alive
```
As you can see, the Terraform agent has started and has output some log
data. From the log data, you can see that our agent is running in server mode,
and has claimed leadership of the cluster. Additionally, the local member has
been marked as a healthy member of the cluster.
<div class="alert alert-block alert-warning">
<strong>Note for OS X Users:</strong> Terraform uses your hostname as the
default node name. If your hostname contains periods, DNS queries to
that node will not work with Terraform. To avoid this, explicitly set
the name of your node with the <code>-node</code> flag.
</div>
## Cluster Members
If you run `terraform members` in another terminal, you can see the members of
the Terraform cluster. You should only see one member (yourself). We'll cover
joining clusters in the next section.
```
$ terraform members
Armons-MacBook-Air 10.1.10.38:8301 alive role=terraform,dc=dc1,vsn=1,vsn_min=1,vsn_max=1,port=8300,bootstrap=1
```
The output shows our own node, the address it is running on, its
health state, and some metadata associated with the node. Some important
metadata keys to recognize are the `role` and `dc` keys. These tell you
the service name and the datacenter that member is within. These can be
used to lookup nodes and services using the DNS interface, which is covered
shortly.
The output from the `members` command is generated based on the
[gossip protocol](/docs/internals/gossip.html) and is eventually consistent.
For a strongly consistent view of the world, use the
[HTTP API](/docs/agent/http.html), which forwards the request to the
Terraform servers:
```
$ curl localhost:8500/v1/catalog/nodes
[{"Node":"Armons-MacBook-Air","Address":"10.1.10.38"}]
```
In addition to the HTTP API, the
[DNS interface](/docs/agent/dns.html) can be used to query the node. Note
that you have to make sure to point your DNS lookups to the Terraform agent's
DNS server, which runs on port 8600 by default. The format of the DNS
entries (such as "Armons-MacBook-Air.node.terraform") will be covered later.
```
$ dig @127.0.0.1 -p 8600 Armons-MacBook-Air.node.terraform
...
;; QUESTION SECTION:
;Armons-MacBook-Air.node.terraform. IN A
;; ANSWER SECTION:
Armons-MacBook-Air.node.terraform. 0 IN A 10.1.10.38
```
## Stopping the Agent
You can use `Ctrl-C` (the interrupt signal) to gracefully halt the agent.
After interrupting the agent, you should see it leave the cluster gracefully
and shut down.
By gracefully leaving, Terraform notifies other cluster members that the
node _left_. If you had forcibly killed the agent process, other members
of the cluster would have detected that the node _failed_. When a member leaves,
its services and checks are removed from the catalog. When a member fails,
its health is simply marked as critical, but is not removed from the catalog.
Terraform will automatically try to reconnect to _failed_ nodes, which allows it
to recover from certain network conditions, while _left_ nodes are no longer contacted.
Additionally, if an agent is operating as a server, a graceful leave is important
to avoid causing a potential availability outage affecting the [consensus protocol](/docs/internals/consensus.html).
See the [guides section](/docs/guides/index.html) to safely add and remove servers.

View File

@ -0,0 +1,203 @@
---
layout: "intro"
page_title: "Build Infrastructure"
sidebar_current: "gettingstarted-build"
---
# Build Infrastructure
With Terraform installed, let's dive right into it and start creating
some infrastructure.
We'll build infrastructure on
[AWS](http://aws.amazon.com) for the getting started guide
since it is popular and generally understood, but Terraform
can [manage many providers](#),
including multiple providers in a single configuration.
Some examples of this are in the
[use cases section](/intro/use-cases.html).
If you don't have an AWS account,
[create one now](http://aws.amazon.com/free/).
For the getting started guide, we'll only be using resources
which qualify under the AWS
[free-tier](http://aws.amazon.com/free/),
meaning it will be free.
If you already have an AWS account, you may be charged some
amount of money, but it shouldn't be more than a few dollars
at most.
<div class="alert alert-block alert-warning">
<p>
<strong>Note:</strong> If you're not using an account that qualifies
under the AWS
<a href="http://aws.amazon.com/free/">free-tier</a>,
you may be charged to run these examples. The most you should
be charged should only be a few dollars, but we're not responsible
for any charges that may incur.
</p>
</div>
## Configuration
The set of files used to describe infrastructure in Terraform is simply
known as a Terraform _configuration_. We're going to write our first
configuration now to launch a single AWS EC2 instance.
The format of the configuration files is
[documented here](#).
Configuration files can
[also be JSON](#), but we recommend only using JSON when the
configuration is generated by a machine.
The entire configuration is shown below. We'll go over each part
after. Save the contents to a file named `example.tf`. Verify that
there are no other `*.tf` files in your directory, since Terraform
loads all of them.
```
provider "aws" {
access_key = "ACCESS_KEY_HERE"
secret_key = "SECRET_KEY_HERE"
region = "us-east-1"
}
resource "aws_instance" "example" {
ami = "ami-408c7f28"
instance_type = "t1.micro"
}
```
Replace the `ACCESS_KEY_HERE` and `SECRET_KEY_HERE` with your
AWS access key and secret key, available from
[this page](https://console.aws.amazon.com/iam/home?#security_credential).
We're hardcoding them for now, but will extract these into
variables later in the getting started guide.
This is a complete configuration that Terraform is ready to apply.
The general structure should be intuitive and straightforward.
The `provider` block is used to configure the named provider, in
our case "aws." A provider is responsible for creating and
managing resources. Multiple provider blocks can exist if a
Terraform configuration is comprised of multiple providers,
which is a common situation.
The `resource` block defines a resource that exists within
the infrastructure. A resource might be a physical component such
as an EC2 instance, or it can be a logical resource such as
a Heroku applcation.
The resource block has two strings before opening the block:
the resource type and the resource name. In our example, the
resource type is "aws\_instance" and the name is "example."
The prefix of the type maps to the provider. In our case
"aws\_instance" automatically tells Terraform that it is
managed by the "aws" provider.
Within the resource block itself is configuration for that
resource. This is dependent on each resource provider and
is fully documented within our
[providers reference](#). For our EC2 instance, we specify
an AMI for Ubuntu, and request a "t1.micro" instance so we
qualify under the free tier.
## Execution Plan
Next, let's see what Terraform would do if we asked it to
apply this configuration. In the same directory as the
`example.tf` file you created, run `terraform plan`. You
should see output similar to what is copied below. We've
truncated some of the output to save space.
```
$ terraform plan
...
+ aws_instance.example
ami: "" => "ami-408c7f28"
availability_zone: "" => "<computed>"
instance_type: "" => "t1.micro"
key_name: "" => "<computed>"
private_dns: "" => "<computed>"
private_ip: "" => "<computed>"
public_dns: "" => "<computed>"
public_ip: "" => "<computed>"
security_groups: "" => "<computed>"
subnet_id: "" => "<computed>"
```
`terraform plan` shows what changes Terraform will apply to
your infrastructure given the current state of your infrastructure
as well as the current contents of your configuration.
If `terraform plan` failed with an error, read the error message
and fix the error that occurred. At this stage, it is probably a
syntax error in the configuration.
The output format is similar to the diff format generated by tools
such as Git. The output has a "+" next to "aws\_instance.example",
meaning that Terraform will create this resource. Beneath that,
it shows the attributes that will be set. When the value it is
going to is `<computed>`, it means that the value won't be known
until the resource is created.
## Apply
The plan looks good, our configuration appears valid, so its time to
create real resources. Run `terraform apply` in the same directory
as your `example.tf`, and watch it go! It will take a few minutes
since Terraform waits for the EC2 instance to become available.
```
$ terraform apply
aws_instance.example: Creating...
ami: "" => "ami-408c7f28"
instance_type: "" => "t1.micro"
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
...
```
Done! You can go to the AWS console to prove to yourself that the
EC2 instance has been created.
Terraform also put some state into the `terraform.tfstate` file
by default. This state file is extremely important; it maps various
resource metadata to actual resource IDs so that Terraform knows
what it is managing. This file must be saved and distributed
to anyone who might run Terraform. We recommend simply putting it
into version control, since it generally isn't too large.
You can inspect the state using `terraform show`:
```
$ terraform show
aws_instance.example:
id = i-e60900cd
ami = ami-408c7f28
availability_zone = us-east-1c
instance_type = t1.micro
key_name =
private_dns = domU-12-31-39-12-38-AB.compute-1.internal
private_ip = 10.200.59.89
public_dns = ec2-54-81-21-192.compute-1.amazonaws.com
public_ip = 54.81.21.192
security_groups.# = 1
security_groups.0 = default
subnet_id =
```
You can see that by creating our resource, we've also gathered
a lot more metadata about it. This metadata can actually be referenced
for other resources or outputs, which will be covered later in
the getting started guide.
## Next
Congratulations! You've built your first infrastructure with Terraform.
You've seen the configuration syntax, an example of a basic execution
plan, and understand the state file.
Next, we're going to move on to changing and destroying infrastructure.

View File

@ -0,0 +1,95 @@
---
layout: "intro"
page_title: "Change Infrastructure"
sidebar_current: "gettingstarted-change"
---
# Change Infrastructure
In the previous page, you created your first infrastructure with
Terraform: a single EC2 instance. In this page, we're going to
modify that resource, and see how Terraform handles change.
Infrastructure is continuously evolving, and Terraform was built
to help manage and enact that change. As you change Terraform
configurations, Terraform builds an execution plan that only
modifies what is necessary to reach your desired state.
By using Terraform to change infrastructure, you can version
control not only your configurations but also your state so you
can see how the infrastructure evolved over time.
## Configuration
Let's modify the `ami` of our instance. Edit the "aws\_instance.web"
resource in your configuration and change it to the following:
```
resource "aws_instance" "example" {
ami = "ami-aa7ab6c2"
instance_type = "t1.micro"
}
```
We've changed the AMI from being an Ubuntu 14.04 AMI to being
an Ubuntu 12.04 AMI. Terraform configurations are meant to be
changed like this. You can also completely remove resources
and Terraform will know to destroy the old one.
## Execution Plan
Let's see what Terraform will do with the change we made.
```
$ terraform plan
...
-/+ aws_instance.example
ami: "ami-408c7f28" => "ami-aa7ab6c2" (forces new resource)
availability_zone: "us-east-1c" => "<computed>"
key_name: "" => "<computed>"
private_dns: "domU-12-31-39-12-38-AB.compute-1.internal" => "<computed>"
private_ip: "10.200.59.89" => "<computed>"
public_dns: "ec2-54-81-21-192.compute-1.amazonaws.com" => "<computed>"
public_ip: "54.81.21.192" => "<computed>"
security_groups: "" => "<computed>"
subnet_id: "" => "<computed>"
```
The prefix "-/+" means that Terraform will destroy and recreate
the resource, versus purely updating it in-place. While some attributes
can do in-place updates (which are shown with a "~" prefix), AMI
changing on EC2 instance requires a new resource. Terraform handles
these details for you, and the execution plan makes it clear what
Terraform will do.
Additionally, the plan output shows that the AMI change is what
necessitated the creation of a new resource. Using this information,
you can tweak your changes to possibly avoid destroy/create updates
if you didn't want to do them at this time.
## Apply
From the plan, we know what will happen. Let's apply and enact
the change.
```
$ terraform apply
aws_instance.example: Destroying...
aws_instance.example: Modifying...
ami: "ami-408c7f28" => "ami-aa7ab6c2"
Apply complete! Resources: 0 added, 1 changed, 1 destroyed.
...
```
As the plan predicted, Terraform started by destroying our old
instance, then creating the new one. You can use `terraform show`
again to see the new properties associated with this instance.
## Next
You've now seen how easy it is to modify infrastructure with
Terraform. Feel free to play around with this more before continuing.
In the next section we're going to destroy our infrastructure.

View File

@ -1,94 +0,0 @@
---
layout: "intro"
page_title: "Registering Health Checks"
sidebar_current: "gettingstarted-checks"
---
# Health Checks
We've now seen how simple it is to run Terraform, add nodes and services, and
query those nodes and services. In this section we will continue by adding
health checks to both nodes and services, a critical component of service
discovery that prevents using services that are unhealthy.
This page will build upon the previous page and assumes you have a
two node cluster running.
## Defining Checks
Similarly to a service, a check can be registered either by providing a
[check definition](/docs/agent/checks.html)
, or by making the appropriate calls to the
[HTTP API](/docs/agent/http.html).
We will use the check definition, because just like services, definitions
are the most common way to setup checks.
Create two definition files in the Terraform configuration directory of
the second node.
The first file will add a host-level check, and the second will modify the web
service definition to add a service-level check.
```
$ echo '{"check": {"name": "ping", "script": "ping -c1 google.com >/dev/null", "interval": "30s"}}' >/etc/terraform.d/ping.json
$ echo '{"service": {"name": "web", "tags": ["rails"], "port": 80,
"check": {"script": "curl localhost:80 >/dev/null 2>&1", "interval": "10s"}}}' >/etc/terraform.d/web.json
```
The first definition adds a host-level check named "ping". This check runs
on a 30 second interval, invoking `ping -c1 google.com`. If the command
exits with a non-zero exit code, then the node will be flagged unhealthy.
The second command modifies the web service and adds a check that uses
curl every 10 seconds to verify that the web server is running.
Restart the second agent, or send a `SIGHUP` to it. We should now see the
following log lines:
```
==> Starting Terraform agent...
...
[INFO] agent: Synced service 'web'
[INFO] agent: Synced check 'service:web'
[INFO] agent: Synced check 'ping'
[WARN] Check 'service:web' is now critical
```
The first few log lines indicate that the agent has synced the new
definitions. The last line indicates that the check we added for
the `web` service is critical. This is because we're not actually running
a web server and the curl test is failing!
## Checking Health Status
Now that we've added some simple checks, we can use the HTTP API to check
them. First, we can look for any failing checks. You can run this curl
on either node:
```
$ curl http://localhost:8500/v1/health/state/critical
[{"Node":"agent-two","CheckID":"service:web","Name":"Service 'web' check","Status":"critical","Notes":"","ServiceID":"web","ServiceName":"web"}]
```
We can see that there is only a single check in the `critical` state, which is
our `web` service check.
Additionally, we can attempt to query the web service using DNS. Terraform
will not return any results, since the service is unhealthy:
```
dig @127.0.0.1 -p 8600 web.service.terraform
...
;; QUESTION SECTION:
;web.service.terraform. IN A
```
This section should have shown that checks can be easily added. Check definitions
can be updated by changing configuration files and sending a `SIGHUP` to the agent.
Alternatively the HTTP API can be used to add, remove and modify checks dynamically.
The API allows for a "dead man's switch" or [TTL based check](/docs/agent/checks.html).
TTL checks can be used to integrate an application more tightly with Terraform, enabling
business logic to be evaluated as part of passing a check.

View File

@ -0,0 +1,68 @@
---
layout: "intro"
page_title: "Destroy Infrastructure"
sidebar_current: "gettingstarted-destroy"
---
# Destroy Infrastructure
We've now seen how to build and change infrastructure. Before we
move on to creating multiple resources and showing resource
dependencies, we're going to go over how to completely destroy
the Terraform-managed infrastructure.
Destroying your infrastructure is a rare event in production
environments. But if you're using Terraform to spin up multiple
environments such as development, test, QA environments, then
destroying is a useful action.
## Plan
While our infrastructure is simple, viewing the execution plan
of a destroy can be useful to make sure that it is destroying
only the resources you expect.
To ask Terraform to create an execution plan to destroy all
infrastructure, run the plan command with the `-destroy` flag.
```
$ terraform plan -destroy
...
- aws_instance.example
```
The output says that "aws\_instance.example" will be deleted.
The `-destroy` flag lets you destroy infrastructure without
modifying the configuration. You can also destroy infrastructure
by simply commenting out or deleting the contents of your
configuration, but usually you just want to destroy an instance
of your infrastructure rather than permanently deleting your
configuration as well. The `-destroy` flag is for this case.
## Apply
Let's apply the destroy:
```
$ terraform apply -destroy
aws_instance.example: Destroying...
Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
...
```
Done. Terraform destroyed our one instance, and if you run a
`terraform show`, you'll see that the state file is now empty.
## Next
You now know how to create, modify, and destroy infrastructure.
With these building blocks, you can effectively experiment with
any part of Terraform.
Next, we move on to features that make Terraform configurations
slightly more useful: variables, resource dependencies, provisioning,
and more.

View File

@ -6,36 +6,25 @@ sidebar_current: "gettingstarted-install"
# Install Terraform # Install Terraform
Terraform must first be installed on every node that will be a member of a Terraform must first be installed on your machine. Terraform is distributed
Terraform cluster. To make installation easy, Terraform is distributed as a as a [binary package](/downloads.html) for all supported platforms and
[binary package](/downloads.html) for all supported platforms and architecture. This page will not cover how to compile Terraform from
architectures. This page will not cover how to compile Terraform from
source. source.
## Installing Terraform ## Installing Terraform
To install Terraform, find the [appropriate package](/downloads.html) for To install Terraform, find the [appropriate package](/downloads.html) for
your system and download it. Terraform is packaged as a "zip" archive. your system and download it. Terraform is packaged as a zip archive.
After downloading Terraform, unzip the package. Copy the `terraform` binary to After downloading Terraform, unzip the package into a directory where
somewhere on the PATH so that it can be executed. On Unix systems, Terraform will be installed. The directory will contain a set of binary
`~/bin` and `/usr/local/bin` are common installation directories, programs, such as `terraform`, `terraform-provider-aws`, etc. The final
depending on if you want to restrict the install to a single user or step is to make sure the directory you installed Terraform to is on the
expose it to the entire system. On Windows systems, you can put it wherever PATH. See
you would like. [this page](http://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux)
for instructions on setting the PATH on Linux and Mac.
### OS X [This page](http://stackoverflow.com/questions/1618280/where-can-i-set-path-to-make-exe-on-windows)
contains instructions for setting the PATH on Windows.
If you are using [homebrew](http://brew.sh/#install) as a package manager,
than you can install terraform as simple as:
```
brew cask install terraform
```
if you are missing the [cask plugin](http://caskroom.io/) you can install it with:
```
brew install caskroom/cask/brew-cask
```
## Verifying the Installation ## Verifying the Installation
@ -48,15 +37,13 @@ $ terraform
usage: terraform [--version] [--help] <command> [<args>] usage: terraform [--version] [--help] <command> [<args>]
Available commands are: Available commands are:
agent Runs a Terraform agent apply Builds or changes infrastructure
force-leave Forces a member of the cluster to enter the "left" state graph Create a visual graph of Terraform resources
info Provides debugging information for operators output Read an output from a state file
join Tell Terraform agent to join cluster plan Generate and show an execution plan
keygen Generates a new encryption key refresh Update local state file against real resources
leave Gracefully leaves the Terraform cluster and shuts down show Inspect Terraform state or plan
members Lists the members of a Terraform cluster version Prints the Terraform version
monitor Stream logs from a Terraform agent
version Prints the Terraform version
``` ```
If you get an error that `terraform` could not be found, then your PATH If you get an error that `terraform` could not be found, then your PATH

View File

@ -1,121 +0,0 @@
---
layout: "intro"
page_title: "Terraform Cluster"
sidebar_current: "gettingstarted-join"
---
# Terraform Cluster
By this point, we've started our first agent and registered and queried
one or more services on that agent. This showed how easy it is to use
Terraform, but didn't show how this could be extended to a scalable production
service discovery infrastructure. On this page, we'll create our first
real cluster with multiple members.
When starting a Terraform agent, it begins without knowledge of any other node, and is
an isolated cluster of one. To learn about other cluster members, the agent must
_join_ an existing cluster. To join an existing cluster, only needs to know
about a _single_ existing member. After it joins, the agent will gossip with this
member and quickly discover the other members in the cluster. A Terraform
agent can join any other agent, it doesn't have to be an agent in server mode.
## Starting the Agents
To simulate a more realistic cluster, we are using a two node cluster in
Vagrant. The Vagrantfile can be found in the demo section of the repo
[here](https://github.com/hashicorp/terraform/tree/master/demo/vagrant-cluster).
We start the first agent on our first node and also specify a node name.
The node name must be unique and is how a machine is uniquely identified.
By default it is the hostname of the machine, but we'll manually override it.
We are also providing a bind address. This is the address that Terraform listens on,
and it *must* be accessible by all other nodes in the cluster. The first node
will act as our server in this cluster. We're still not making a cluster
of servers.
```
$ terraform agent -server -bootstrap -data-dir /tmp/consul \
-node=agent-one -bind=172.20.20.10
...
```
Then, in another terminal, start the second agent on the new node.
This time, we set the bind address to match the IP of the second node
as specified in the Vagrantfile. In production, you will generally want
to provide a bind address or interface as well.
```
$ terraform agent -data-dir /tmp/consul -node=agent-two -bind=172.20.20.11
...
```
At this point, you have two Terraform agents running, one server and one client.
The two Terraform agents still don't know anything about each other, and are each part of their own
clusters (of one member). You can verify this by running `terraform members`
against each agent and noting that only one member is a part of each.
## Joining a Cluster
Now, let's tell the first agent to join the second agent by running
the following command in a new terminal:
```
$ terraform join 172.20.20.11
Successfully joined cluster by contacting 1 nodes.
```
You should see some log output in each of the agent logs. If you read
carefully, you'll see that they received join information. If you
run `terraform members` against each agent, you'll see that both agents now
know about each other:
```
$ terraform members
agent-one 172.20.20.10:8301 alive role=terraform,dc=dc1,vsn=1,vsn_min=1,vsn_max=1,port=8300,bootstrap=1
agent-two 172.20.20.11:8301 alive role=node,dc=dc1,vsn=1,vsn_min=1,vsn_max=1
```
<div class="alert alert-block alert-info">
<p><strong>Remember:</strong> To join a cluster, a Terraform agent needs to only
learn about <em>one existing member</em>. After joining the cluster, the
agents gossip with each other to propagate full membership information.
</p>
</div>
In addition to using `terraform join` you can use the `-join` flag on
`terraform agent` to join a cluster as part of starting up the agent.
## Querying Nodes
Just like querying services, Terraform has an API for querying the
nodes themselves. You can do this via the DNS or HTTP API.
For the DNS API, the structure of the names is `NAME.node.terraform` or
`NAME.DATACENTER.node.terraform`. If the datacenter is omitted, Terraform
will only search the local datacenter.
From "agent-one", query "agent-two":
```
$ dig @127.0.0.1 -p 8600 agent-two.node.terraform
...
;; QUESTION SECTION:
;agent-two.node.terraform. IN A
;; ANSWER SECTION:
agent-two.node.terraform. 0 IN A 172.20.20.11
```
The ability to look up nodes in addition to services is incredibly
useful for system administration tasks. For example, knowing the address
of the node to SSH into is as easy as making it part of the Terraform cluster
and querying it.
## Leaving a Cluster
To leave the cluster, you can either gracefully quit an agent (using
`Ctrl-C`) or force kill one of the agents. Gracefully leaving allows
the node to transition into the _left_ state, otherwise other nodes
will detect it as having _failed_. The difference is covered
in more detail [here](/intro/getting-started/agent.html#toc_3).

View File

@ -1,118 +0,0 @@
---
layout: "intro"
page_title: "Key/Value Data"
sidebar_current: "gettingstarted-kv"
---
# Key/Value Data
In addition to providing service discovery and integrated health checking,
Terraform provides an easy to use Key/Value store. This can be used to hold
dynamic configuration, assist in service coordination, build leader election,
and anything else a developer can think to build. The
[HTTP API](/docs/agent/http.html) fully documents the features of the K/V store.
This page assumes you have at least one Terraform agent already running.
## Simple Usage
To demonstrate how simple it is to get started, we will manipulate a few keys
in the K/V store.
Querying the agent we started in a prior page, we can first verify that
there are no existing keys in the k/v store:
```
$ curl -v http://localhost:8500/v1/kv/?recurse
* About to connect() to localhost port 8500 (#0)
* Trying 127.0.0.1... connected
> GET /v1/kv/?recurse HTTP/1.1
> User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
> Host: localhost:8500
> Accept: */*
>
< HTTP/1.1 404 Not Found
< X-Terraform-Index: 1
< Date: Fri, 11 Apr 2014 02:10:28 GMT
< Content-Length: 0
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
* Closing connection #0
```
Since there are no keys, we get a 404 response back.
Now, we can put a few example keys:
```
$ curl -X PUT -d 'test' http://localhost:8500/v1/kv/web/key1
true
$ curl -X PUT -d 'test' http://localhost:8500/v1/kv/web/key2?flags=42
true
$ curl -X PUT -d 'test' http://localhost:8500/v1/kv/web/sub/key3
true
$ curl http://localhost:8500/v1/kv/?recurse
[{"CreateIndex":97,"ModifyIndex":97,"Key":"web/key1","Flags":0,"Value":"dGVzdA=="},
{"CreateIndex":98,"ModifyIndex":98,"Key":"web/key2","Flags":42,"Value":"dGVzdA=="},
{"CreateIndex":99,"ModifyIndex":99,"Key":"web/sub/key3","Flags":0,"Value":"dGVzdA=="}]
```
Here we have created 3 keys, each with the value of "test". Note that the
`Value` field returned is base64 encoded to allow non-UTF8
characters. For the "web/key2" key, we set a `flag` value of 42. All keys
support setting a 64bit integer flag value. This is opaque to Terraform but can
be used by clients for any purpose.
After setting the values, we then issued a GET request to retrieve multiple
keys using the `?recurse` parameter.
You can also fetch a single key just as easily:
```
$ curl http://localhost:8500/v1/kv/web/key1
[{"CreateIndex":97,"ModifyIndex":97,"Key":"web/key1","Flags":0,"Value":"dGVzdA=="}]
```
Deleting keys is simple as well. We can delete a single key by specifying the
full path, or we can recursively delete all keys under a root using "?recurse":
```
$ curl -X DELETE http://localhost:8500/v1/kv/web/sub?recurse
$ curl http://localhost:8500/v1/kv/web?recurse
[{"CreateIndex":97,"ModifyIndex":97,"Key":"web/key1","Flags":0,"Value":"dGVzdA=="},
{"CreateIndex":98,"ModifyIndex":98,"Key":"web/key2","Flags":42,"Value":"dGVzdA=="}]
```
A key can be updated by setting a new value by issuing the same PUT request.
Additionally, Terraform provides a Check-And-Set operation, enabling atomic
key updates. This is done by providing the `?cas=` paramter with the last
`ModifyIndex` value from the GET request. For example, suppose we wanted
to update "web/key1":
```
$ curl -X PUT -d 'newval' http://localhost:8500/v1/kv/web/key1?cas=97
true
$ curl -X PUT -d 'newval' http://localhost:8500/v1/kv/web/key1?cas=97
false
```
In this case, the first CAS update succeeds because the last modify time is 97.
However the second operation fails because the `ModifyIndex` is no longer 97.
We can also make use of the `ModifyIndex` to wait for a key's value to change.
For example, suppose we wanted to wait for key2 to be modified:
```
$ curl "http://localhost:8500/v1/kv/web/key2?index=101&wait=5s"
[{"CreateIndex":98,"ModifyIndex":101,"Key":"web/key2","Flags":42,"Value":"dGVzdA=="}]
```
By providing "?index=" we are asking to wait until the key has a `ModifyIndex` greater
than 101. However the "?wait=5s" parameter restricts the query to at most 5 seconds,
returning the current, unchanged value. This can be used to efficiently wait for
key modifications. Additionally, this same technique can be used to wait for a list
of keys, waiting only until any of the keys has a newer modification time.
This is only a few example of what the API supports. For full documentation, please
reference the [HTTP API](/docs/agent/http.html).

View File

@ -1,139 +0,0 @@
---
layout: "intro"
page_title: "Registering Services"
sidebar_current: "gettingstarted-services"
---
# Registering Services
In the previous page, we ran our first agent, saw the cluster members, and
queried that node. On this page, we'll register our first service and query
that service. We're not yet running a cluster of Terraform agents.
## Defining a Service
A service can be registered either by providing a
[service definition](/docs/agent/services.html),
or by making the appropriate calls to the
[HTTP API](/docs/agent/http.html).
We're going to start by registering a service using a service definition,
since this is the most common way that services are registered. We'll be
building on what we covered in the
[previous page](/intro/getting-started/agent.html).
First, create a directory for Terraform configurations. A good directory
is typically `/etc/terraform.d`. Terraform loads all configuration files in the
configuration directory.
```
$ sudo mkdir /etc/terraform.d
```
Next, we'll write a service definition configuration file. We'll
pretend we have a service named "web" running on port 80. Additionally,
we'll give it some tags, which we can use as additional ways to query
it later.
```
$ echo '{"service": {"name": "web", "tags": ["rails"], "port": 80}}' \
>/etc/terraform.d/web.json
```
Now, restart the agent we're running, providing the configuration directory:
```
$ terraform agent -server -bootstrap -data-dir /tmp/consul -config-dir /etc/consul.d
==> Starting Terraform agent...
...
[INFO] agent: Synced service 'web'
...
```
You'll notice in the output that it "synced" the web service. This means
that it loaded the information from the configuration.
If you wanted to register multiple services, you create multiple service
definition files in the Terraform configuration directory.
## Querying Services
Once the agent is started and the service is synced, we can query that
service using either the DNS or HTTP API.
### DNS API
Let's first query it using the DNS API. For the DNS API, the DNS name
for services is `NAME.service.terraform`. All DNS names are always in the
`terraform` namespace. The `service` subdomain on that tells Terraform we're
querying services, and the `NAME` is the name of the service. For the
web service we registered, that would be `web.service.terraform`:
```
$ dig @127.0.0.1 -p 8600 web.service.terraform
...
;; QUESTION SECTION:
;web.service.terraform. IN A
;; ANSWER SECTION:
web.service.terraform. 0 IN A 172.20.20.11
```
As you can see, an A record was returned with the IP address of the node that
the service is available on. A records can only hold IP addresses. You can
also use the DNS API to retrieve the entire address/port pair using SRV
records:
```
$ dig @127.0.0.1 -p 8600 web.service.terraform SRV
...
;; QUESTION SECTION:
;web.service.terraform. IN SRV
;; ANSWER SECTION:
web.service.terraform. 0 IN SRV 1 1 80 agent-one.node.dc1.consul.
;; ADDITIONAL SECTION:
agent-one.node.dc1.terraform. 0 IN A 172.20.20.11
```
The SRV record returned says that the web service is running on port 80
and exists on the node `agent-one.node.dc1.terraform.`. An additional section
is returned by the DNS with the A record for that node.
Finally, we can also use the DNS API to filter services by tags. The
format for tag-based service queries is `TAG.NAME.service.terraform`. In
the example below, we ask Terraform for all web services with the "rails"
tag. We get a response since we registered our service with that tag.
```
$ dig @127.0.0.1 -p 8600 rails.web.service.terraform
...
;; QUESTION SECTION:
;rails.web.service.terraform. IN A
;; ANSWER SECTION:
rails.web.service.terraform. 0 IN A 172.20.20.11
```
### HTTP API
In addition to the DNS API, the HTTP API can be used to query services:
```
$ curl http://localhost:8500/v1/catalog/service/web
[{"Node":"agent-one","Address":"172.20.20.11","ServiceID":"web","ServiceName":"web","ServiceTags":["rails"],"ServicePort":80}]
```
## Updating Services
Service definitions can be updated by changing configuration files and
sending a `SIGHUP` to the agent. This lets you update services without
any downtime or unavailability to service queries.
Alternatively the HTTP API can be used to add, remove, and modify services
dynamically.

View File

@ -1,56 +0,0 @@
---
layout: "intro"
page_title: "Web UI"
sidebar_current: "gettingstarted-ui"
---
# Terraform Web UI
Terraform comes with support for a
[beautiful, functional web UI](http://demo.terraform.io) out of the box.
This UI can be used for viewing all services and nodes, viewing all
health checks and their current status, and for reading and setting
key/value data. The UI automatically supports multi-datacenter.
For ease of deployment, the UI is
[distributed](/downloads_web_ui.html)
as static HTML and JavaScript.
You don't need a separate web server to run the web UI. The Terraform
agent itself can be configured to serve the UI.
## Screenshot and Demo
You can view a live demo of the Terraform Web UI
[here](http://demo.terraform.io).
While the live demo is able to access data from all datacenters,
we've also setup demo endpoints in the specific datacenters:
[AMS2](http://ams2.demo.terraform.io) (Amsterdam),
[SFO1](http://sfo1.demo.terraform.io) (San Francisco),
and [NYC1](http://nyc1.demo.terraform.io) (New York).
A screenshot of one page of the demo is shown below so you can get an
idea of what the web UI is like. Click the screenshot for the full size.
<div class="center">
<a href="/images/terraform_web_ui.png">
<img src="/images/terraform_web_ui.png">
</a>
</div>
## Set Up
To set up the web UI,
[download the web UI package](/downloads_web_ui.html)
and unzip it to a directory somewhere on the server where the Terraform agent
is also being run. Then, just append the `-ui-dir` to the `terraform agent`
command pointing to the directory where you unzipped the UI (the
directory with the `index.html` file):
```
$ terraform agent -ui-dir /path/to/ui
...
```
The UI is available at the `/ui` path on the same port as the HTTP API.
By default this is `http://localhost:8500/ui`.

View File

@ -6,70 +6,66 @@ sidebar_current: "what"
# Introduction to Terraform # Introduction to Terraform
Welcome to the intro guide to Terraform! This guide is the best place to start Welcome to the intro guide to Terraform! This guide is the best
with Terraform. We cover what Terraform is, what problems it can solve, how it compares place to start with Terraform. We cover what Terraform is, what
to existing software, and a quick start for using Terraform. If you are already familiar problems it can solve, how it compares to existing software,
with the basics of Terraform, the [documentation](/docs/index.html) provides more and contains a quick start for using Terraform.
of a reference for all available features.
If you are already familiar with the basics of Terraform, the
[documentation](/docs/index.html) provides a better reference
guide for all available features as well as internals.
## What is Terraform? ## What is Terraform?
Terraform has multiple components, but as a whole, it is a tool for discovering Terraform is a tool for building, changing, and versioning infrastructure
and configuring services in your infrastructure. It provides several safely and efficiently. Terraform can manage existing and popular service
key features: providers as well as custom in-house solutions.
* **Service Discovery**: Clients of Terraform can _provide_ a service, such as Configuration files describe to Terraform the components needed to
`api` or `mysql`, and other clients can use Terraform to _discover_ providers run a single application or your entire datacenter.
of a given service. Using either DNS or HTTP, applications can easily find Terraform generates an execution plan describing
the services they depend upon. what it will do to reach the desired state, and then executes it to build the
described infrastructure. As the configuration changes, Terraform is able
to determine what changed and create incremental execution plans which
can be applied.
* **Health Checking**: Terraform clients can provide any number of health checks, The infrastructure Terraform can manage includes
either associated with a given service ("is the webserver returning 200 OK"), or low-level components such as
with the local node ("is memory utilization below 90%"). This information can be compute instances, storage, and networking, as well as high-level
used by an operator to monitor cluster health, and it is used by the service components such as DNS entries, SaaS features, etc.
discovery components to route traffic away from unhealthy hosts.
* **Key/Value Store**: Applications can make use of Terraform's hierarchical key/value Examples work best to showcase Terraform. Please see the
store for any number of purposes including: dynamic configuration, feature flagging, [use cases](/intro/use-cases.html).
coordination, leader election, etc. The simple HTTP API makes it easy to use.
* **Multi Datacenter**: Terraform supports multiple datacenters out of the box. This The key features of Terraform are:
means users of Terraform do not have to worry about building additional layers of
abstraction to grow to multiple regions.
Terraform is designed to be friendly to both the DevOps community and * **Infrastructure as Code**: Infrastructure is described using a high-level
application developers, making it perfect for modern, elastic infrastructures. configuration syntax. This allows a blueprint of your datacenter to be
versioned and treated as you would any other code. Additionally,
infrastructure can be shared and re-used.
## Basic Architecture of Terraform * **Execution Plans**: Terraform has a "planning" step where it generates
an _execution plan_. The execution plan shows what Terraform will do when
you call apply. This lets you avoid any surprises when Terraform
manipulates infrastructure.
Terraform is a distributed, highly available system. There is an * **Resource Graph**: Terraform builds a graph of all your resources,
[in-depth architecture overview](/docs/internals/architecture.html) available, and parallelizes the creation and modification of any non-dependent
but this section will cover the basics so you can get an understanding resources. Because of this, Terraform builds infrastructure as efficiently
of how Terraform works. This section will purposely omit details to quickly as possible, and operators get insight into dependencies in their
provide an overview of the architecture. infrastructure.
Every node that provides services to Terraform runs a _Terraform agent_. Running * **Change Automation**: Potentially complex changesets are applied to
an agent is not required for discovering other services or getting/setting your infrastructure with minimal human interaction.
key/value data. The agent is responsible for health checking the services With the previously mentioned execution
on the node as well as the node itself. plan and resource graph, you know exactly what Terraform will change
and in what order, avoiding many possible human errors.
The agents talk to one or more _Terraform servers_. The Terraform servers are
where data is stored and replicated. The servers themselves elect a leader.
While Terraform can function with one server, 3 to 5 is recommended to avoid
data loss scenarios. A cluster of Terraform servers is recommended for each
datacenter.
Components of your infrastructure that need to discover other services
or nodes can query any of the Terraform servers _or_ any of the Terraform agents.
The agents forward queries to the servers automatically.
Each datacenter runs a cluster of Terraform servers. When a cross-datacenter
service discovery or configuration request is made, the local Terraform servers
forward the request to the remote datacenter and return the result.
## Next Steps ## Next Steps
See the page on [how Terraform compares to other software](/intro/vs/index.html) See the page on [Terraform use cases](/intro/use-cases.html) to see the
to see how it fits into your existing infrastructure. Or continue onwards with multiple ways Terraform can be used. Then see
[how Terraform compares to other software](/intro/vs/index.html)
to see how it fits into your existing infrastructure. Finally, continue onwards with
the [getting started guide](/intro/getting-started/install.html) to get the [getting started guide](/intro/getting-started/install.html) to get
Terraform up and running and see how it works. Terraform managing some real infrastructure and to see how it works.

View File

@ -0,0 +1,70 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<div class="docs-sidebar hidden-print affix-top" role="complementary">
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/index.html">&laquo; Documentation Home</a>
</li>
<li<%= sidebar_current("docs-aws-index") %>>
<a href="/docs/providers/aws/index.html">AWS Provider</a>
</li>
<li<%= sidebar_current("docs-aws-resource") %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-aws-resource-eip") %>>
<a href="/docs/providers/aws/r/eip.html">aws_eip</a>
</li>
<li<%= sidebar_current("docs-aws-resource-instance") %>>
<a href="/docs/providers/aws/r/instance.html">aws_instance</a>
</li>
<li<%= sidebar_current("docs-aws-resource-internet-gateway") %>>
<a href="/docs/providers/aws/r/internet_gateway.html">aws_internet_gateway</a>
</li>
<li<%= sidebar_current("docs-aws-resource-launch-config") %>>
<a href="/docs/providers/aws/r/launch_config.html">aws_launch_configuration</a>
</li>
<li<%= sidebar_current("docs-aws-resource-route-table|") %>>
<a href="/docs/providers/aws/r/route_table.html">aws_route_table</a>
</li>
<li<%= sidebar_current("docs-aws-resource-route-table-assoc") %>>
<a href="/docs/providers/aws/r/route_table_assoc.html">aws_route_table_association</a>
</li>
<li<%= sidebar_current("docs-aws-resource-route53-record") %>>
<a href="/docs/providers/aws/r/route53_record.html">aws_route53_record</a>
</li>
<li<%= sidebar_current("docs-aws-resource-route53-zone") %>>
<a href="/docs/providers/aws/r/route53_zone.html">aws_route53_zone</a>
</li>
<li<%= sidebar_current("docs-aws-resource-s3-bucket") %>>
<a href="/docs/providers/aws/r/s3_bucket.html">aws_s3_bucket</a>
</li>
<li<%= sidebar_current("docs-aws-resource-security-group") %>>
<a href="/docs/providers/aws/r/security_group.html">aws_security_group</a>
</li>
<li<%= sidebar_current("docs-aws-resource-subnet") %>>
<a href="/docs/providers/aws/r/subnet.html">aws_subnet</a>
</li>
<li<%= sidebar_current("docs-aws-resource-vpc") %>>
<a href="/docs/providers/aws/r/vpc.html">aws_vpc</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>

View File

@ -4,52 +4,16 @@
<ul class="nav docs-sidenav"> <ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>> <li<%= sidebar_current("docs-home") %>>
<a href="/docs/index.html">Documentation Home</a> <a href="/docs/index.html">Documentation Home</a>
</li> </li>
<li<%= sidebar_current("docs-upgrading") %>> <li<%= sidebar_current("docs-config") %>>
<a href="/docs/upgrading.html">Upgrading and Compatibility</a> <a href="/docs/configuration/index.html">Configuration</a>
<ul class="nav">
<li<%= sidebar_current("docs-upgrading-upgrading") %>>
<a href="/docs/upgrading.html">Upgrading Terraform</a>
</li>
<li<%= sidebar_current("docs-upgrading-compat") %>>
<a href="/docs/compatibility.html">Compatibility Promise</a>
</li>
</ul>
</li>
<li<%= sidebar_current("docs-internals") %>>
<a href="/docs/internals/index.html">Terraform Internals</a>
<ul class="nav"> <ul class="nav">
<li<%= sidebar_current("docs-internals-architecture") %>> </ul>
<a href="/docs/internals/architecture.html">Architecture</a> </li>
</li>
<li<%= sidebar_current("docs-internals-consensus") %>>
<a href="/docs/internals/consensus.html">Consensus Protocol</a>
</li>
<li<%= sidebar_current("docs-internals-gossip") %>>
<a href="/docs/internals/gossip.html">Gossip Protocol</a>
</li>
<li<%= sidebar_current("docs-internals-sessions") %>>
<a href="/docs/internals/sessions.html">Sessions</a>
</li>
<li<%= sidebar_current("docs-internals-security") %>>
<a href="/docs/internals/security.html">Security Model</a>
</li>
<li<%= sidebar_current("docs-internals-jepsen") %>>
<a href="/docs/internals/jepsen.html">Jepsen Testing</a>
</li>
</ul>
</li>
<li<%= sidebar_current("docs-commands") %>> <li<%= sidebar_current("docs-commands") %>>
<a href="/docs/commands/index.html">Terraform Commands (CLI)</a> <a href="/docs/commands/index.html">Commands (CLI)</a>
<ul class="nav"> <ul class="nav">
<li<%= sidebar_current("docs-commands-agent") %>> <li<%= sidebar_current("docs-commands-agent") %>>
<a href="/docs/commands/agent.html">agent</a> <a href="/docs/commands/agent.html">agent</a>
@ -85,45 +49,43 @@
</ul> </ul>
</li> </li>
<li<%= sidebar_current("docs-agent") %>> <li<%= sidebar_current("docs-providers") %>>
<a href="/docs/agent/basics.html">Terraform Agent</a> <a href="/docs/providers/index.html">Providers</a>
<ul class="nav"> <ul class="nav">
<li<%= sidebar_current("docs-agent-running") %>> <li<%= sidebar_current("docs-providers-aws") %>>
<a href="/docs/agent/basics.html">Running and Stopping</a> <a href="/docs/providers/aws/index.html">AWS</a>
</li> </li>
<li<%= sidebar_current("docs-agent-dns") %>> <li<%= sidebar_current("docs-providers-do") %>>
<a href="/docs/agent/dns.html">DNS Interface</a> <a href="/docs/providers/do/index.html">DigitalOcean</a>
</li> </li>
<li<%= sidebar_current("docs-agent-http") %>> <li<%= sidebar_current("docs-providers-heroku") %>>
<a href="/docs/agent/http.html">HTTP API</a> <a href="/docs/providers/heroku/index.html">Heroku</a>
</li>
<li<%= sidebar_current("docs-agent-config") %>>
<a href="/docs/agent/options.html">Configuration</a>
</li> </li>
<li<%= sidebar_current("docs-agent-services") %>>
<a href="/docs/agent/services.html">Service Definitions</a>
</li>
<li<%= sidebar_current("docs-agent-checks") %>>
<a href="/docs/agent/checks.html">Check Definitions</a>
</li>
<li<%= sidebar_current("docs-agent-encryption") %>>
<a href="/docs/agent/encryption.html">Encryption</a>
</li>
<li<%= sidebar_current("docs-agent-rpc") %>>
<a href="/docs/agent/rpc.html">RPC Protocol</a>
</li>
<li<%= sidebar_current("docs-agent-telemetry") %>>
<a href="/docs/agent/telemetry.html">Telemetry</a>
</li>
</ul> </ul>
</li>
<li<%= sidebar_current("docs-provisioners") %>>
<a href="/docs/provisioners/index.html">Provisioners</a>
<ul class="nav">
<li<%= sidebar_current("docs-provisioners-connection") %>>
<a href="/docs/provisioners/connection.html">connection</a>
</li>
<li<%= sidebar_current("docs-provisioners-file") %>>
<a href="/docs/provisioners/file.html">file</a>
</li>
<li<%= sidebar_current("docs-provisioners-local") %>>
<a href="/docs/provisioners/local-exec.html">local-exec</a>
</li>
<li<%= sidebar_current("docs-provisioners-remote") %>>
<a href="/docs/provisioners/remote-exec.html">remote-exec</a>
</li>
</ul>
</li>
<li<%= sidebar_current("docs-guides") %>> <li<%= sidebar_current("docs-guides") %>>
<a href="/docs/guides/index.html">Guides</a> <a href="/docs/guides/index.html">Guides</a>
@ -161,6 +123,34 @@
</li> </li>
</ul> </ul>
<li<%= sidebar_current("docs-internals") %>>
<a href="/docs/internals/index.html">Internals</a>
<ul class="nav">
<li<%= sidebar_current("docs-internals-architecture") %>>
<a href="/docs/internals/architecture.html">Architecture</a>
</li>
<li<%= sidebar_current("docs-internals-consensus") %>>
<a href="/docs/internals/consensus.html">Consensus Protocol</a>
</li>
<li<%= sidebar_current("docs-internals-gossip") %>>
<a href="/docs/internals/gossip.html">Gossip Protocol</a>
</li>
<li<%= sidebar_current("docs-internals-sessions") %>>
<a href="/docs/internals/sessions.html">Sessions</a>
</li>
<li<%= sidebar_current("docs-internals-security") %>>
<a href="/docs/internals/security.html">Security Model</a>
</li>
<li<%= sidebar_current("docs-internals-jepsen") %>>
<a href="/docs/internals/jepsen.html">Jepsen Testing</a>
</li>
</ul>
</li>
</ul> </ul>
</div> </div>
<% end %> <% end %>

View File

@ -4,34 +4,18 @@
<ul class="nav docs-sidenav"> <ul class="nav docs-sidenav">
<li<%= sidebar_current("what") %>> <li<%= sidebar_current("what") %>>
<a href="/intro/index.html">What is Terraform?</a> <a href="/intro/index.html">What is Terraform?</a>
</li>
<li<%= sidebar_current("use-cases") %>>
<a href="/intro/index.html">Use Cases</a>
</li> </li>
<li<%= sidebar_current("vs-other") %>> <li<%= sidebar_current("vs-other") %>>
<a href="/intro/vs/index.html">Terraform vs. Other Software</a> <a href="/intro/vs/index.html">Terraform vs. Other Software</a>
<ul class="nav"> <ul class="nav">
<li<%= sidebar_current("vs-other-zk") %>>
<a href="/intro/vs/zookeeper.html">ZooKeeper, doozerd, etcd</a>
</li>
<li<%= sidebar_current("vs-other-chef") %>> <li<%= sidebar_current("vs-other-chef") %>>
<a href="/intro/vs/chef-puppet.html">Chef, Puppet, etc.</a> <a href="/intro/vs/chef-puppet.html">Chef, Puppet, etc.</a>
</li> </li>
<li<%= sidebar_current("vs-other-nagios-sensu") %>>
<a href="/intro/vs/nagios-sensu.html">Nagios, Sensu</a>
</li>
<li<%= sidebar_current("vs-other-skydns") %>>
<a href="/intro/vs/skydns.html">SkyDNS</a>
</li>
<li<%= sidebar_current("vs-other-smartstack") %>>
<a href="/intro/vs/smartstack.html">SmartStack</a>
</li>
<li<%= sidebar_current("vs-other-serf") %>>
<a href="/intro/vs/serf.html">Serf</a>
</li>
<li<%= sidebar_current("vs-other-custom") %>> <li<%= sidebar_current("vs-other-custom") %>>
<a href="/intro/vs/custom.html">Custom Solutions</a> <a href="/intro/vs/custom.html">Custom Solutions</a>
@ -46,34 +30,37 @@
<a href="/intro/getting-started/install.html">Install Terraform</a> <a href="/intro/getting-started/install.html">Install Terraform</a>
</li> </li>
<li<%= sidebar_current("gettingstarted-agent") %>> <li<%= sidebar_current("gettingstarted-build") %>>
<a href="/intro/getting-started/agent.html">Run the Agent</a> <a href="/intro/getting-started/build.html">Build Infrastructure</a>
</li> </li>
<li<%= sidebar_current("gettingstarted-services") %>> <li<%= sidebar_current("gettingstarted-change") %>>
<a href="/intro/getting-started/services.html">Services</a> <a href="/intro/getting-started/change.html">Change Infrastructure</a>
</li> </li>
<li<%= sidebar_current("gettingstarted-join") %>> <li<%= sidebar_current("gettingstarted-destroy") %>>
<a href="/intro/getting-started/join.html">Terraform Cluster</a> <a href="/intro/getting-started/destroy.html">Destroy Infrastructure</a>
</li> </li>
<li<%= sidebar_current("gettingstarted-checks") %>> <li<%= sidebar_current("gettingstarted-outputs") %>>
<a href="/intro/getting-started/checks.html">Health Checks</a> <a href="/intro/getting-started/outputs.html">Output Variables</a>
</li> </li>
<li<%= sidebar_current("gettingstarted-kv") %>> <li<%= sidebar_current("gettingstarted-deps") %>>
<a href="/intro/getting-started/kv.html">Key/Value Data</a> <a href="/intro/getting-started/dependencies.html">Resource Dependencies</a>
</li> </li>
<li<%= sidebar_current("gettingstarted-ui") %>> <li<%= sidebar_current("gettingstarted-variables") %>>
<a href="/intro/getting-started/ui.html">Web UI</a> <a href="/intro/getting-started/variables.html">Input Variables</a>
</li>
<li<%= sidebar_current("gettingstarted-variables") %>>
<a href="/intro/getting-started/provisioners.html">Provision</a>
</li> </li>
<li<%= sidebar_current("gettingstarted-nextsteps") %>> <li<%= sidebar_current("gettingstarted-nextsteps") %>>
<a href="/intro/getting-started/next-steps.html">Next Steps</a> <a href="/intro/getting-started/next-steps.html">Next Steps</a>
</li> </li>
</ul> </ul>
</li> </li>
</ul> </ul>

View File

@ -6,7 +6,9 @@ body.page-sub{
background-color: @light-black; background-color: @light-black;
} }
body.layout-aws,
body.layout-docs, body.layout-docs,
body.layout-inner,
body.layout-intro{ body.layout-intro{
background: @light-black url('../images/sidebar-wire.png') left 62px no-repeat; background: @light-black url('../images/sidebar-wire.png') left 62px no-repeat;
@ -127,6 +129,10 @@ body.layout-intro{
} }
} }
} }
.nav-visible {
display: block;
}
} }
} }
@ -192,6 +198,7 @@ body.layout-intro{
@media (max-width: 992px) { @media (max-width: 992px) {
body.layout-docs, body.layout-docs,
body.layout-inner,
body.layout-intro{ body.layout-intro{
>.container{ >.container{
.col-md-8[role=main]{ .col-md-8[role=main]{

View File

@ -993,16 +993,22 @@ body.page-home #footer {
body.page-sub { body.page-sub {
background-color: #242424; background-color: #242424;
} }
body.layout-aws,
body.layout-docs, body.layout-docs,
body.layout-inner,
body.layout-intro { body.layout-intro {
background: #242424 url('../images/sidebar-wire.png') left 62px no-repeat; background: #242424 url('../images/sidebar-wire.png') left 62px no-repeat;
} }
body.layout-aws > .container .col-md-8[role=main],
body.layout-docs > .container .col-md-8[role=main], body.layout-docs > .container .col-md-8[role=main],
body.layout-inner > .container .col-md-8[role=main],
body.layout-intro > .container .col-md-8[role=main] { body.layout-intro > .container .col-md-8[role=main] {
min-height: 800px; min-height: 800px;
background-color: white; background-color: white;
} }
body.layout-aws > .container .col-md-8[role=main] > div,
body.layout-docs > .container .col-md-8[role=main] > div, body.layout-docs > .container .col-md-8[role=main] > div,
body.layout-inner > .container .col-md-8[role=main] > div,
body.layout-intro > .container .col-md-8[role=main] > div { body.layout-intro > .container .col-md-8[role=main] > div {
position: relative; position: relative;
z-index: 10; z-index: 10;
@ -1096,6 +1102,9 @@ body.layout-intro > .container .col-md-8[role=main] > div {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
padding: 6px 15px; padding: 6px 15px;
} }
.docs-sidebar .docs-sidenav .nav-visible {
display: block;
}
.bs-docs-section { .bs-docs-section {
padding-top: 10px; padding-top: 10px;
padding-left: 3%; padding-left: 3%;
@ -1151,10 +1160,12 @@ body.layout-intro > .container .col-md-8[role=main] > div {
} }
@media (max-width: 992px) { @media (max-width: 992px) {
body.layout-docs > .container .col-md-8[role=main], body.layout-docs > .container .col-md-8[role=main],
body.layout-inner > .container .col-md-8[role=main],
body.layout-intro > .container .col-md-8[role=main] { body.layout-intro > .container .col-md-8[role=main] {
min-height: 0; min-height: 0;
} }
body.layout-docs > .container .col-md-8[role=main]::before, body.layout-docs > .container .col-md-8[role=main]::before,
body.layout-inner > .container .col-md-8[role=main]::before,
body.layout-intro > .container .col-md-8[role=main]::before { body.layout-intro > .container .col-md-8[role=main]::before {
border-left: 9999px solid white; border-left: 9999px solid white;
} }

View File

@ -26,4 +26,4 @@
// Components w/ JavaScript // Components w/ JavaScript
/*@import "modals.less";*/ /*@import "modals.less";*/
// 68 // 69