Skip to content

Commit 0b421b6

Browse files
committed
provider/aws: Add aws_alb resource
This commit adds a resource, acceptance tests and documentation for the new Application Load Balancer (aws_alb). We choose to use the name alb over the package name, elbv2, in order to avoid confusion. This is the first in a series of commits to fully support the new resources necessary for Application Load Balancers.
1 parent ebdfe76 commit 0b421b6

5 files changed

Lines changed: 749 additions & 1 deletion

File tree

builtin/providers/aws/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ func Provider() terraform.ResourceProvider {
152152
},
153153

154154
ResourcesMap: map[string]*schema.Resource{
155+
"aws_alb": resourceAwsAlb(),
155156
"aws_ami": resourceAwsAmi(),
156157
"aws_ami_copy": resourceAwsAmiCopy(),
157158
"aws_ami_from_instance": resourceAwsAmiFromInstance(),
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
package aws
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"strconv"
7+
8+
"github.com/aws/aws-sdk-go/aws"
9+
"github.com/aws/aws-sdk-go/service/elbv2"
10+
"github.com/hashicorp/errwrap"
11+
"github.com/hashicorp/terraform/helper/schema"
12+
)
13+
14+
func resourceAwsAlb() *schema.Resource {
15+
return &schema.Resource{
16+
Create: resourceAwsAlbCreate,
17+
Read: resourceAwsAlbRead,
18+
Update: resourceAwsAlbUpdate,
19+
Delete: resourceAwsAlbDelete,
20+
Importer: &schema.ResourceImporter{
21+
State: schema.ImportStatePassthrough,
22+
},
23+
24+
Schema: map[string]*schema.Schema{
25+
"name": {
26+
Type: schema.TypeString,
27+
Required: true,
28+
ForceNew: true,
29+
ValidateFunc: validateElbName,
30+
},
31+
32+
"internal": {
33+
Type: schema.TypeBool,
34+
Optional: true,
35+
ForceNew: true,
36+
Computed: true,
37+
},
38+
39+
"security_groups": {
40+
Type: schema.TypeSet,
41+
Elem: &schema.Schema{Type: schema.TypeString},
42+
ForceNew: true,
43+
Optional: true,
44+
Set: schema.HashString,
45+
},
46+
47+
"subnets": {
48+
Type: schema.TypeSet,
49+
Elem: &schema.Schema{Type: schema.TypeString},
50+
ForceNew: true,
51+
Required: true,
52+
Set: schema.HashString,
53+
},
54+
55+
"access_logs": {
56+
Type: schema.TypeList,
57+
Optional: true,
58+
MaxItems: 1,
59+
Elem: &schema.Resource{
60+
Schema: map[string]*schema.Schema{
61+
"bucket": {
62+
Type: schema.TypeString,
63+
Required: true,
64+
},
65+
"prefix": {
66+
Type: schema.TypeString,
67+
Optional: true,
68+
},
69+
},
70+
},
71+
},
72+
73+
"enable_deletion_protection": {
74+
Type: schema.TypeBool,
75+
Optional: true,
76+
Default: false,
77+
},
78+
79+
"idle_timeout": {
80+
Type: schema.TypeInt,
81+
Optional: true,
82+
Default: 60,
83+
},
84+
85+
"vpc_id": {
86+
Type: schema.TypeString,
87+
Computed: true,
88+
},
89+
90+
"zone_id": {
91+
Type: schema.TypeString,
92+
Computed: true,
93+
},
94+
95+
"dns_name": {
96+
Type: schema.TypeString,
97+
Computed: true,
98+
},
99+
100+
"tags": tagsSchema(),
101+
},
102+
}
103+
}
104+
105+
func resourceAwsAlbCreate(d *schema.ResourceData, meta interface{}) error {
106+
elbconn := meta.(*AWSClient).elbv2conn
107+
108+
elbOpts := &elbv2.CreateLoadBalancerInput{
109+
Name: aws.String(d.Get("name").(string)),
110+
Tags: tagsFromMapELBv2(d.Get("tags").(map[string]interface{})),
111+
}
112+
113+
if scheme, ok := d.GetOk("internal"); ok && scheme.(bool) {
114+
elbOpts.Scheme = aws.String("internal")
115+
}
116+
117+
if v, ok := d.GetOk("security_groups"); ok {
118+
elbOpts.SecurityGroups = expandStringList(v.(*schema.Set).List())
119+
}
120+
121+
if v, ok := d.GetOk("subnets"); ok {
122+
elbOpts.Subnets = expandStringList(v.(*schema.Set).List())
123+
}
124+
125+
log.Printf("[DEBUG] ALB create configuration: %#v", elbOpts)
126+
127+
resp, err := elbconn.CreateLoadBalancer(elbOpts)
128+
if err != nil {
129+
return errwrap.Wrapf("Error creating Application Load Balancer: {{err}}", err)
130+
}
131+
132+
if len(resp.LoadBalancers) != 1 {
133+
return fmt.Errorf("No load balancers returned following creation of %s", d.Get("name").(string))
134+
}
135+
136+
d.SetId(*resp.LoadBalancers[0].LoadBalancerArn)
137+
log.Printf("[INFO] ALB ID: %s", d.Id())
138+
139+
return resourceAwsAlbUpdate(d, meta)
140+
}
141+
142+
func resourceAwsAlbRead(d *schema.ResourceData, meta interface{}) error {
143+
elbconn := meta.(*AWSClient).elbv2conn
144+
albArn := d.Id()
145+
146+
describeAlbOpts := &elbv2.DescribeLoadBalancersInput{
147+
LoadBalancerArns: []*string{aws.String(albArn)},
148+
}
149+
150+
describeResp, err := elbconn.DescribeLoadBalancers(describeAlbOpts)
151+
if err != nil {
152+
if isLoadBalancerNotFound(err) {
153+
// The ALB is gone now, so just remove it from the state
154+
log.Printf("[WARN] ALB %s not found in AWS, removing from state", d.Id())
155+
d.SetId("")
156+
return nil
157+
}
158+
159+
return errwrap.Wrapf("Error retrieving ALB: {{err}}", err)
160+
}
161+
if len(describeResp.LoadBalancers) != 1 {
162+
return fmt.Errorf("Unable to find ALB: %#v", describeResp.LoadBalancers)
163+
}
164+
165+
alb := describeResp.LoadBalancers[0]
166+
167+
d.Set("name", alb.LoadBalancerName)
168+
d.Set("internal", (alb.Scheme != nil && *alb.Scheme == "internal"))
169+
d.Set("security_groups", flattenStringList(alb.SecurityGroups))
170+
d.Set("subnets", flattenSubnetsFromAvailabilityZones(alb.AvailabilityZones))
171+
d.Set("vpc_id", alb.VpcId)
172+
d.Set("zone_id", alb.CanonicalHostedZoneId)
173+
d.Set("dns_name", alb.DNSName)
174+
175+
respTags, err := elbconn.DescribeTags(&elbv2.DescribeTagsInput{
176+
ResourceArns: []*string{alb.LoadBalancerArn},
177+
})
178+
if err != nil {
179+
return errwrap.Wrapf("Error retrieving ALB Tags: {{err}}", err)
180+
}
181+
182+
var et []*elbv2.Tag
183+
if len(respTags.TagDescriptions) > 0 {
184+
et = respTags.TagDescriptions[0].Tags
185+
}
186+
d.Set("tags", tagsToMapELBv2(et))
187+
188+
attributesResp, err := elbconn.DescribeLoadBalancerAttributes(&elbv2.DescribeLoadBalancerAttributesInput{
189+
LoadBalancerArn: aws.String(d.Id()),
190+
})
191+
if err != nil {
192+
return errwrap.Wrapf("Error retrieving ALB Attributes: {{err}}", err)
193+
}
194+
195+
accessLogMap := map[string]interface{}{}
196+
for _, attr := range attributesResp.Attributes {
197+
switch *attr.Key {
198+
case "access_logs.s3.bucket":
199+
accessLogMap["bucket"] = *attr.Value
200+
case "access_logs.s3.prefix":
201+
accessLogMap["prefix"] = *attr.Value
202+
case "idle_timeout.timeout_seconds":
203+
timeout, err := strconv.Atoi(*attr.Value)
204+
if err != nil {
205+
return errwrap.Wrapf("Error parsing ALB timeout: {{err}}", err)
206+
}
207+
log.Printf("[DEBUG] Setting ALB Timeout Seconds: %d", timeout)
208+
d.Set("idle_timeout", timeout)
209+
case "deletion_protection.enabled":
210+
protectionEnabled := (*attr.Value) == "true"
211+
log.Printf("[DEBUG] Setting ALB Deletion Protection Enabled: %t", protectionEnabled)
212+
d.Set("enable_deletion_protection", protectionEnabled)
213+
}
214+
}
215+
216+
log.Printf("[DEBUG] Setting ALB Access Logs: %#v", accessLogMap)
217+
if accessLogMap["bucket"] != "" || accessLogMap["prefix"] != "" {
218+
d.Set("access_logs", []interface{}{accessLogMap})
219+
} else {
220+
d.Set("access_logs", []interface{}{})
221+
}
222+
223+
return nil
224+
}
225+
226+
func resourceAwsAlbUpdate(d *schema.ResourceData, meta interface{}) error {
227+
elbconn := meta.(*AWSClient).elbv2conn
228+
229+
attributes := make([]*elbv2.LoadBalancerAttribute, 0)
230+
231+
if d.HasChange("access_logs") {
232+
logs := d.Get("access_logs").([]interface{})
233+
if len(logs) == 1 {
234+
log := logs[0].(map[string]interface{})
235+
236+
attributes = append(attributes,
237+
&elbv2.LoadBalancerAttribute{
238+
Key: aws.String("access_logs.s3.enabled"),
239+
Value: aws.String("true"),
240+
},
241+
&elbv2.LoadBalancerAttribute{
242+
Key: aws.String("access_logs.s3.bucket"),
243+
Value: aws.String(log["bucket"].(string)),
244+
})
245+
246+
if prefix, ok := log["prefix"]; ok {
247+
attributes = append(attributes, &elbv2.LoadBalancerAttribute{
248+
Key: aws.String("access_logs.s3.prefix"),
249+
Value: aws.String(prefix.(string)),
250+
})
251+
}
252+
} else if len(logs) == 0 {
253+
attributes = append(attributes, &elbv2.LoadBalancerAttribute{
254+
Key: aws.String("access_logs.s3.enabled"),
255+
Value: aws.String("false"),
256+
})
257+
}
258+
}
259+
260+
if d.HasChange("enable_deletion_protection") {
261+
attributes = append(attributes, &elbv2.LoadBalancerAttribute{
262+
Key: aws.String("deletion_protection.enabled"),
263+
Value: aws.String(fmt.Sprintf("%t", d.Get("enable_deletion_protection").(bool))),
264+
})
265+
}
266+
267+
if d.HasChange("idle_timeout") {
268+
attributes = append(attributes, &elbv2.LoadBalancerAttribute{
269+
Key: aws.String("idle_timeout.timeout_seconds"),
270+
Value: aws.String(fmt.Sprintf("%d", d.Get("idle_timeout").(int))),
271+
})
272+
}
273+
274+
if len(attributes) != 0 {
275+
input := &elbv2.ModifyLoadBalancerAttributesInput{
276+
LoadBalancerArn: aws.String(d.Id()),
277+
Attributes: attributes,
278+
}
279+
280+
log.Printf("[DEBUG] ALB Modify Load Balancer Attributes Request: %#v", input)
281+
_, err := elbconn.ModifyLoadBalancerAttributes(input)
282+
if err != nil {
283+
return fmt.Errorf("Failure configuring ALB attributes: %s", err)
284+
}
285+
}
286+
287+
return resourceAwsAlbRead(d, meta)
288+
}
289+
290+
func resourceAwsAlbDelete(d *schema.ResourceData, meta interface{}) error {
291+
albconn := meta.(*AWSClient).elbv2conn
292+
293+
log.Printf("[INFO] Deleting ALB: %s", d.Id())
294+
295+
// Destroy the load balancer
296+
deleteElbOpts := elbv2.DeleteLoadBalancerInput{
297+
LoadBalancerArn: aws.String(d.Id()),
298+
}
299+
if _, err := albconn.DeleteLoadBalancer(&deleteElbOpts); err != nil {
300+
return fmt.Errorf("Error deleting ALB: %s", err)
301+
}
302+
303+
return nil
304+
}
305+
306+
// tagsToMapELBv2 turns the list of tags into a map.
307+
func tagsToMapELBv2(ts []*elbv2.Tag) map[string]string {
308+
result := make(map[string]string)
309+
for _, t := range ts {
310+
result[*t.Key] = *t.Value
311+
}
312+
313+
return result
314+
}
315+
316+
// tagsFromMapELBv2 returns the tags for the given map of data.
317+
func tagsFromMapELBv2(m map[string]interface{}) []*elbv2.Tag {
318+
var result []*elbv2.Tag
319+
for k, v := range m {
320+
result = append(result, &elbv2.Tag{
321+
Key: aws.String(k),
322+
Value: aws.String(v.(string)),
323+
})
324+
}
325+
326+
return result
327+
}
328+
329+
// flattenSubnetsFromAvailabilityZones creates a slice of strings containing the subnet IDs
330+
// for the ALB based on the AvailabilityZones structure returned by the API.
331+
func flattenSubnetsFromAvailabilityZones(availabilityZones []*elbv2.AvailabilityZone) []string {
332+
var result []string
333+
for _, az := range availabilityZones {
334+
result = append(result, *az.SubnetId)
335+
}
336+
return result
337+
}

0 commit comments

Comments
 (0)