provider/aws: Add Security Group Rule as a top level resource

- document conflict with sg rules and sg in-line rules
- for this to work, ingress rules need to be computed
This commit is contained in:
Clint Shryock 2015-04-20 13:38:21 -05:00
parent 96e91c1cbd
commit 885efa0837
8 changed files with 861 additions and 54 deletions

View File

@ -120,6 +120,7 @@ func Provider() terraform.ResourceProvider {
"aws_route_table": resourceAwsRouteTable(), "aws_route_table": resourceAwsRouteTable(),
"aws_s3_bucket": resourceAwsS3Bucket(), "aws_s3_bucket": resourceAwsS3Bucket(),
"aws_security_group": resourceAwsSecurityGroup(), "aws_security_group": resourceAwsSecurityGroup(),
"aws_security_group_rule": resourceAwsSecurityGroupRule(),
"aws_subnet": resourceAwsSubnet(), "aws_subnet": resourceAwsSubnet(),
"aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(), "aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(),
"aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(), "aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(),

View File

@ -45,6 +45,7 @@ func resourceAwsSecurityGroup() *schema.Resource {
"ingress": &schema.Schema{ "ingress": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true,
Elem: &schema.Resource{ Elem: &schema.Resource{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"from_port": &schema.Schema{ "from_port": &schema.Schema{

View File

@ -0,0 +1,342 @@
package aws
import (
"bytes"
"fmt"
"log"
"sort"
"strings"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsSecurityGroupRule() *schema.Resource {
return &schema.Resource{
Create: resourceAwsSecurityGroupRuleCreate,
Read: resourceAwsSecurityGroupRuleRead,
Delete: resourceAwsSecurityGroupRuleDelete,
Schema: map[string]*schema.Schema{
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Type of rule, ingress (inbound) or egress (outbound).",
},
"from_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},
"to_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},
"protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"cidr_blocks": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"security_group_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"source_security_group_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
"self": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
ForceNew: true,
},
},
}
}
func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
sg_id := d.Get("security_group_id").(string)
sg, err := findResourceSecurityGroup(conn, sg_id)
if err != nil {
return fmt.Errorf("sorry")
}
perm := expandIPPerm(d, sg)
ruleType := d.Get("type").(string)
switch ruleType {
case "ingress":
log.Printf("[DEBUG] Authorizing security group %s %s rule: %#v",
sg_id, "Ingress", perm)
req := &ec2.AuthorizeSecurityGroupIngressInput{
GroupID: sg.GroupID,
IPPermissions: []*ec2.IPPermission{perm},
}
if sg.VPCID == nil || *sg.VPCID == "" {
req.GroupID = nil
req.GroupName = sg.GroupName
}
_, err := conn.AuthorizeSecurityGroupIngress(req)
if err != nil {
return fmt.Errorf(
"Error authorizing security group %s rules: %s",
"rules", err)
}
case "egress":
log.Printf("[DEBUG] Authorizing security group %s %s rule: %#v",
sg_id, "Egress", perm)
req := &ec2.AuthorizeSecurityGroupEgressInput{
GroupID: sg.GroupID,
IPPermissions: []*ec2.IPPermission{perm},
}
_, err = conn.AuthorizeSecurityGroupEgress(req)
if err != nil {
return fmt.Errorf(
"Error authorizing security group %s rules: %s",
"rules", err)
}
default:
return fmt.Errorf("Security Group Rule must be type 'ingress' or type 'egress'")
}
d.SetId(ipPermissionIDHash(ruleType, perm))
return resourceAwsSecurityGroupRuleRead(d, meta)
}
func resourceAwsSecurityGroupRuleRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
sg_id := d.Get("security_group_id").(string)
sg, err := findResourceSecurityGroup(conn, sg_id)
if err != nil {
d.SetId("")
}
var rule *ec2.IPPermission
ruleType := d.Get("type").(string)
var rl []*ec2.IPPermission
switch ruleType {
case "ingress":
rl = sg.IPPermissions
default:
rl = sg.IPPermissionsEgress
}
for _, r := range rl {
if d.Id() == ipPermissionIDHash(ruleType, r) {
rule = r
}
}
if rule == nil {
log.Printf("[DEBUG] Unable to find matching %s Security Group Rule for Group %s",
ruleType, sg_id)
d.SetId("")
return nil
}
d.Set("from_port", rule.FromPort)
d.Set("to_port", rule.ToPort)
d.Set("protocol", rule.IPProtocol)
d.Set("type", ruleType)
var cb []string
for _, c := range rule.IPRanges {
cb = append(cb, *c.CIDRIP)
}
d.Set("cidr_blocks", cb)
if len(rule.UserIDGroupPairs) > 0 {
s := rule.UserIDGroupPairs[0]
d.Set("source_security_group_id", *s.GroupID)
}
return nil
}
func resourceAwsSecurityGroupRuleDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
sg_id := d.Get("security_group_id").(string)
sg, err := findResourceSecurityGroup(conn, sg_id)
if err != nil {
return fmt.Errorf("sorry")
}
perm := expandIPPerm(d, sg)
ruleType := d.Get("type").(string)
switch ruleType {
case "ingress":
log.Printf("[DEBUG] Revoking security group %#v %s rule: %#v",
sg_id, "ingress", perm)
req := &ec2.RevokeSecurityGroupIngressInput{
GroupID: sg.GroupID,
IPPermissions: []*ec2.IPPermission{perm},
}
_, err = conn.RevokeSecurityGroupIngress(req)
if err != nil {
return fmt.Errorf(
"Error revoking security group %s rules: %s",
sg_id, err)
}
case "egress":
log.Printf("[DEBUG] Revoking security group %#v %s rule: %#v",
sg_id, "egress", perm)
req := &ec2.RevokeSecurityGroupEgressInput{
GroupID: sg.GroupID,
IPPermissions: []*ec2.IPPermission{perm},
}
_, err = conn.RevokeSecurityGroupEgress(req)
if err != nil {
return fmt.Errorf(
"Error revoking security group %s rules: %s",
sg_id, err)
}
}
d.SetId("")
return nil
}
func findResourceSecurityGroup(conn *ec2.EC2, id string) (*ec2.SecurityGroup, error) {
req := &ec2.DescribeSecurityGroupsInput{
GroupIDs: []*string{aws.String(id)},
}
resp, err := conn.DescribeSecurityGroups(req)
if err != nil {
if ec2err, ok := err.(aws.APIError); ok {
if ec2err.Code == "InvalidSecurityGroupID.NotFound" ||
ec2err.Code == "InvalidGroup.NotFound" {
resp = nil
err = nil
}
}
if err != nil {
log.Printf("Error on findResourceSecurityGroup: %s", err)
return nil, err
}
}
return resp.SecurityGroups[0], nil
}
func ipPermissionIDHash(ruleType string, ip *ec2.IPPermission) string {
var buf bytes.Buffer
// for egress rules, an TCP rule of -1 is automatically added, in which case
// the to and from ports will be nil. We don't record this rule locally.
if ip.IPProtocol != nil && *ip.IPProtocol != "-1" {
buf.WriteString(fmt.Sprintf("%d-", *ip.FromPort))
buf.WriteString(fmt.Sprintf("%d-", *ip.ToPort))
buf.WriteString(fmt.Sprintf("%s-", *ip.IPProtocol))
}
buf.WriteString(fmt.Sprintf("%s-", ruleType))
// We need to make sure to sort the strings below so that we always
// generate the same hash code no matter what is in the set.
if len(ip.IPRanges) > 0 {
s := make([]string, len(ip.IPRanges))
for i, r := range ip.IPRanges {
s[i] = *r.CIDRIP
}
sort.Strings(s)
for _, v := range s {
buf.WriteString(fmt.Sprintf("%s-", v))
}
}
return fmt.Sprintf("sg-%d", hashcode.String(buf.String()))
}
func expandIPPerm(d *schema.ResourceData, sg *ec2.SecurityGroup) *ec2.IPPermission {
var perm ec2.IPPermission
perm.FromPort = aws.Long(int64(d.Get("from_port").(int)))
perm.ToPort = aws.Long(int64(d.Get("to_port").(int)))
perm.IPProtocol = aws.String(d.Get("protocol").(string))
var groups []string
if raw, ok := d.GetOk("source_security_group_id"); ok {
groups = append(groups, raw.(string))
}
if v, ok := d.GetOk("self"); ok && v.(bool) {
if sg.VPCID != nil && *sg.VPCID != "" {
groups = append(groups, *sg.GroupID)
} else {
groups = append(groups, *sg.GroupName)
}
}
if len(groups) > 0 {
perm.UserIDGroupPairs = make([]*ec2.UserIDGroupPair, len(groups))
for i, name := range groups {
ownerId, id := "", name
if items := strings.Split(id, "/"); len(items) > 1 {
ownerId, id = items[0], items[1]
}
perm.UserIDGroupPairs[i] = &ec2.UserIDGroupPair{
GroupID: aws.String(id),
UserID: aws.String(ownerId),
}
if sg.VPCID == nil || *sg.VPCID == "" {
perm.UserIDGroupPairs[i].GroupID = nil
perm.UserIDGroupPairs[i].GroupName = aws.String(id)
perm.UserIDGroupPairs[i].UserID = nil
}
}
}
if raw, ok := d.GetOk("cidr_blocks"); ok {
list := raw.([]interface{})
perm.IPRanges = make([]*ec2.IPRange, len(list))
for i, v := range list {
perm.IPRanges[i] = &ec2.IPRange{CIDRIP: aws.String(v.(string))}
}
}
return &perm
}

View File

@ -0,0 +1,391 @@
package aws
import (
"fmt"
"reflect"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestIpPermissionIDHash(t *testing.T) {
simple := &ec2.IPPermission{
IPProtocol: aws.String("tcp"),
FromPort: aws.Long(int64(80)),
ToPort: aws.Long(int64(8000)),
IPRanges: []*ec2.IPRange{
&ec2.IPRange{
CIDRIP: aws.String("10.0.0.0/8"),
},
},
}
egress := &ec2.IPPermission{
IPProtocol: aws.String("tcp"),
FromPort: aws.Long(int64(80)),
ToPort: aws.Long(int64(8000)),
IPRanges: []*ec2.IPRange{
&ec2.IPRange{
CIDRIP: aws.String("10.0.0.0/8"),
},
},
}
egress_all := &ec2.IPPermission{
IPProtocol: aws.String("-1"),
IPRanges: []*ec2.IPRange{
&ec2.IPRange{
CIDRIP: aws.String("10.0.0.0/8"),
},
},
}
// hardcoded hashes, to detect future change
cases := []struct {
Input *ec2.IPPermission
Type string
Output string
}{
{simple, "ingress", "sg-82613597"},
{egress, "egress", "sg-363054720"},
{egress_all, "egress", "sg-857124156"},
}
for _, tc := range cases {
actual := ipPermissionIDHash(tc.Type, tc.Input)
if actual != tc.Output {
t.Fatalf("input: %s - %#v\noutput: %s", tc.Type, tc.Input, actual)
}
}
}
func TestAccAWSSecurityGroupRule_Ingress(t *testing.T) {
var group ec2.SecurityGroup
testRuleCount := func(*terraform.State) error {
if len(group.IPPermissions) != 1 {
return fmt.Errorf("Wrong Security Group rule count, expected %d, got %d",
1, len(group.IPPermissions))
}
rule := group.IPPermissions[0]
if *rule.FromPort != int64(80) {
return fmt.Errorf("Wrong Security Group port setting, expected %d, got %d",
80, int(*rule.FromPort))
}
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSecurityGroupRuleDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSecurityGroupRuleIngressConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSecurityGroupRuleExists("aws_security_group.web", &group),
testAccCheckAWSSecurityGroupRuleAttributes(&group, "ingress"),
resource.TestCheckResourceAttr(
"aws_security_group_rule.ingress_1", "from_port", "80"),
testRuleCount,
),
},
},
})
}
func TestAccAWSSecurityGroupRule_IngressClassic(t *testing.T) {
var group ec2.SecurityGroup
testRuleCount := func(*terraform.State) error {
if len(group.IPPermissions) != 1 {
return fmt.Errorf("Wrong Security Group rule count, expected %d, got %d",
1, len(group.IPPermissions))
}
rule := group.IPPermissions[0]
if *rule.FromPort != int64(80) {
return fmt.Errorf("Wrong Security Group port setting, expected %d, got %d",
80, int(*rule.FromPort))
}
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSecurityGroupRuleDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSecurityGroupRuleIngressClassicConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSecurityGroupRuleExists("aws_security_group.web", &group),
testAccCheckAWSSecurityGroupRuleAttributes(&group, "ingress"),
resource.TestCheckResourceAttr(
"aws_security_group_rule.ingress_1", "from_port", "80"),
testRuleCount,
),
},
},
})
}
func TestAccAWSSecurityGroupRule_MultiIngress(t *testing.T) {
var group ec2.SecurityGroup
testMultiRuleCount := func(*terraform.State) error {
if len(group.IPPermissions) != 3 {
return fmt.Errorf("Wrong Security Group rule count, expected %d, got %d",
3, len(group.IPPermissions))
}
var rule *ec2.IPPermission
for _, r := range group.IPPermissions {
if *r.FromPort == int64(80) {
rule = r
}
}
if *rule.ToPort != int64(8000) {
return fmt.Errorf("Wrong Security Group port 2 setting, expected %d, got %d",
8000, int(*rule.ToPort))
}
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSecurityGroupRuleDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSecurityGroupRuleConfigMultiIngress,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSecurityGroupRuleExists("aws_security_group.web", &group),
testMultiRuleCount,
),
},
},
})
}
func TestAccAWSSecurityGroupRule_Egress(t *testing.T) {
var group ec2.SecurityGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSecurityGroupRuleDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSecurityGroupRuleEgressConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSecurityGroupRuleExists("aws_security_group.web", &group),
testAccCheckAWSSecurityGroupRuleAttributes(&group, "egress"),
),
},
},
})
}
func testAccCheckAWSSecurityGroupRuleDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_security_group" {
continue
}
// Retrieve our group
req := &ec2.DescribeSecurityGroupsInput{
GroupIDs: []*string{aws.String(rs.Primary.ID)},
}
resp, err := conn.DescribeSecurityGroups(req)
if err == nil {
if len(resp.SecurityGroups) > 0 && *resp.SecurityGroups[0].GroupID == rs.Primary.ID {
return fmt.Errorf("Security Group (%s) still exists.", rs.Primary.ID)
}
return nil
}
ec2err, ok := err.(aws.APIError)
if !ok {
return err
}
// Confirm error code is what we want
if ec2err.Code != "InvalidGroup.NotFound" {
return err
}
}
return nil
}
func testAccCheckAWSSecurityGroupRuleExists(n string, group *ec2.SecurityGroup) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No Security Group is set")
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
req := &ec2.DescribeSecurityGroupsInput{
GroupIDs: []*string{aws.String(rs.Primary.ID)},
}
resp, err := conn.DescribeSecurityGroups(req)
if err != nil {
return err
}
if len(resp.SecurityGroups) > 0 && *resp.SecurityGroups[0].GroupID == rs.Primary.ID {
*group = *resp.SecurityGroups[0]
return nil
}
return fmt.Errorf("Security Group not found")
}
}
func testAccCheckAWSSecurityGroupRuleAttributes(group *ec2.SecurityGroup, ruleType string) resource.TestCheckFunc {
return func(s *terraform.State) error {
p := &ec2.IPPermission{
FromPort: aws.Long(80),
ToPort: aws.Long(8000),
IPProtocol: aws.String("tcp"),
IPRanges: []*ec2.IPRange{&ec2.IPRange{CIDRIP: aws.String("10.0.0.0/8")}},
}
var rules []*ec2.IPPermission
if ruleType == "ingress" {
rules = group.IPPermissions
} else {
rules = group.IPPermissionsEgress
}
if len(rules) == 0 {
return fmt.Errorf("No IPPerms")
}
// Compare our ingress
if !reflect.DeepEqual(rules[0], p) {
return fmt.Errorf(
"Got:\n\n%#v\n\nExpected:\n\n%#v\n",
rules[0],
p)
}
return nil
}
}
const testAccAWSSecurityGroupRuleIngressConfig = `
resource "aws_security_group" "web" {
name = "terraform_acceptance_test_example"
description = "Used in the terraform acceptance tests"
tags {
Name = "tf-acc-test"
}
}
resource "aws_security_group_rule" "ingress_1" {
type = "ingress"
protocol = "tcp"
from_port = 80
to_port = 8000
cidr_blocks = ["10.0.0.0/8"]
security_group_id = "${aws_security_group.web.id}"
}
`
const testAccAWSSecurityGroupRuleIngressClassicConfig = `
provider "aws" {
region = "us-east-1"
}
resource "aws_security_group" "web" {
name = "terraform_acceptance_test_example"
description = "Used in the terraform acceptance tests"
tags {
Name = "tf-acc-test"
}
}
resource "aws_security_group_rule" "ingress_1" {
type = "ingress"
protocol = "tcp"
from_port = 80
to_port = 8000
cidr_blocks = ["10.0.0.0/8"]
security_group_id = "${aws_security_group.web.id}"
}
`
const testAccAWSSecurityGroupRuleEgressConfig = `
resource "aws_security_group" "web" {
name = "terraform_acceptance_test_example"
description = "Used in the terraform acceptance tests"
tags {
Name = "tf-acc-test"
}
}
resource "aws_security_group_rule" "egress_1" {
type = "egress"
protocol = "tcp"
from_port = 80
to_port = 8000
cidr_blocks = ["10.0.0.0/8"]
security_group_id = "${aws_security_group.web.id}"
}
`
const testAccAWSSecurityGroupRuleConfigMultiIngress = `
resource "aws_security_group" "web" {
name = "terraform_acceptance_test_example_2"
description = "Used in the terraform acceptance tests"
}
resource "aws_security_group" "worker" {
name = "terraform_acceptance_test_example_worker"
description = "Used in the terraform acceptance tests"
}
resource "aws_security_group_rule" "ingress_1" {
type = "ingress"
protocol = "tcp"
from_port = 22
to_port = 22
cidr_blocks = ["10.0.0.0/8"]
security_group_id = "${aws_security_group.web.id}"
source_security_group_id = "${aws_security_group.worker.id}"
}
resource "aws_security_group_rule" "ingress_2" {
type = "ingress"
protocol = "tcp"
from_port = 80
to_port = 8000
self = true
security_group_id = "${aws_security_group.web.id}"
}
`

View File

@ -8,7 +8,14 @@ description: |-
# aws\_security\_group # aws\_security\_group
Provides an security group resource. Provides a security group resource.
~> **NOTE on Security Groups and Security Group Rules:** Terraform currently
provides both a standalone [Security Group Rule resource](security_group_rule.html) (a single `ingress` or
`egress` rule), and a Security Group resource with `ingress` and `egress` rules
defined in-line. At this time you cannot use a Security Group with in-line rules
in conjunction with any Security Group Rule resources. Doing so will cause
a conflict of rule settings and will overwrite rules.
## Example Usage ## Example Usage

View File

@ -0,0 +1,61 @@
---
layout: "aws"
page_title: "AWS: aws_security_group_rule"
sidebar_current: "docs-aws-resource-security-group-rule"
description: |-
Provides an security group rule resource.
---
# aws\_security\_group\_rule
Provides a security group rule resource. Represents a signle `ingress` or
`egress` group rule, which can be added to external Security Groups.
~> **NOTE on Security Groups and Security Group Rules:** Terraform currently
provides both a standalone Security Group Rule resource (a single `ingress` or
`egress` rule), and a [Security Group resource](security_group.html) with `ingress` and `egress` rules
defined in-line. At this time you cannot use a Security Group with in-line rules
in conjunction with any Security Group Rule resources. Doing so will cause
a conflict of rule settings and will overwrite rules.
## Example Usage
Basic usage
```
resource "aws_security_group_rule" "allow_all" {
type = "ingress"
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = "sg-123456"
source_security_group_id = "sg-654321"
}
```
## Argument Reference
The following arguments are supported:
* `type` - (Required) The type of rule being created. Valid options are `ingress` (inbound)
or `egress` (outbound).
* `cidr_blocks` - (Optional) List of CIDR blocks.
* `from_port` - (Required) The start port.
* `protocol` - (Required) The protocol.
* `security_group_id` - (Required) The security group to apply this rule too.
* `source_security_group_id` - (Optional) The security group id to allow access to/from,
depending on the `type`.
* `self` - (Optional) If true, the security group itself will be added as
a source to this ingress rule.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the security group rule
* `type` - The type of rule, `ingress` or `egress`
* `from_port` - The source port
* `to_port` - The destination port
* `protocol`  The protocol used

View File

@ -136,6 +136,10 @@
<a href="/docs/providers/aws/r/security_group.html">aws_security_group</a> <a href="/docs/providers/aws/r/security_group.html">aws_security_group</a>
</li> </li>
<li<%= sidebar_current("docs-aws-resource-security-group-rule") %>>
<a href="/docs/providers/aws/r/security_group_rule.html">aws_security_group_rule</a>
</li>
<li<%= sidebar_current("docs-aws-resource-subnet") %>> <li<%= sidebar_current("docs-aws-resource-subnet") %>>
<a href="/docs/providers/aws/r/subnet.html">aws_subnet</a> <a href="/docs/providers/aws/r/subnet.html">aws_subnet</a>
</li> </li>