provider/aws: Add support for placement_constraint to aws_ecs_service (#11242)

```
% make testacc TEST=./builtin/providers/aws TESTARGS='-run=TestAccAWSEcsServiceWithPlacementConstraints'
==> Checking that code complies with gofmt requirements...
go generate $(go list ./... | grep -v /terraform/vendor/)
2017/01/17 18:25:27 Generated command/internal_plugin_list.go
TF_ACC=1 go test ./builtin/providers/aws -v -run=TestAccAWSEcsServiceWithPlacementConstraints -timeout 120m
=== RUN   TestAccAWSEcsServiceWithPlacementConstraints
--- PASS: TestAccAWSEcsServiceWithPlacementConstraints (113.18s)
PASS
ok  	github.com/hashicorp/terraform/builtin/providers/aws	113.208s
```

//cc @catsby
This commit is contained in:
Paul Stack 2017-01-17 18:43:34 +00:00 committed by GitHub
parent 104d043a01
commit 36b6384956
5 changed files with 178 additions and 62 deletions

View File

@ -26,73 +26,73 @@ func resourceAwsEcsService() *schema.Resource {
Delete: resourceAwsEcsServiceDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"cluster": &schema.Schema{
"cluster": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"task_definition": &schema.Schema{
"task_definition": {
Type: schema.TypeString,
Required: true,
},
"desired_count": &schema.Schema{
"desired_count": {
Type: schema.TypeInt,
Optional: true,
},
"iam_role": &schema.Schema{
"iam_role": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
},
"deployment_maximum_percent": &schema.Schema{
"deployment_maximum_percent": {
Type: schema.TypeInt,
Optional: true,
Default: 200,
},
"deployment_minimum_healthy_percent": &schema.Schema{
"deployment_minimum_healthy_percent": {
Type: schema.TypeInt,
Optional: true,
Default: 100,
},
"load_balancer": &schema.Schema{
"load_balancer": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"elb_name": &schema.Schema{
"elb_name": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"target_group_arn": &schema.Schema{
"target_group_arn": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"container_name": &schema.Schema{
"container_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"container_port": &schema.Schema{
"container_port": {
Type: schema.TypeInt,
Required: true,
ForceNew: true,
@ -102,19 +102,39 @@ func resourceAwsEcsService() *schema.Resource {
Set: resourceAwsEcsLoadBalancerHash,
},
"placement_strategy": &schema.Schema{
"placement_strategy": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
MaxItems: 5,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": &schema.Schema{
"type": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
"field": &schema.Schema{
"field": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
},
},
},
"placement_constraints": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
MaxItems: 10,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": { //TODO: Add a Validation for the types
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
"expression": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
@ -166,6 +186,19 @@ func resourceAwsEcsServiceCreate(d *schema.ResourceData, meta interface{}) error
input.PlacementStrategy = ps
}
constraints := d.Get("placement_constraints").(*schema.Set).List()
if len(constraints) > 0 {
var pc []*ecs.PlacementConstraint
for _, raw := range constraints {
p := raw.(map[string]interface{})
pc = append(pc, &ecs.PlacementConstraint{
Type: aws.String(p["type"].(string)),
Expression: aws.String(p["expression"].(string)),
})
}
input.PlacementConstraints = pc
}
log.Printf("[DEBUG] Creating ECS service: %s", input)
// Retry due to AWS IAM policy eventual consistency
@ -277,10 +310,27 @@ func resourceAwsEcsServiceRead(d *schema.ResourceData, meta interface{}) error {
if err := d.Set("placement_strategy", flattenPlacementStrategy(service.PlacementStrategy)); err != nil {
log.Printf("[ERR] Error setting placement_strategy for (%s): %s", d.Id(), err)
}
if err := d.Set("placement_constraints", flattenServicePlacementConstraints(service.PlacementConstraints)); err != nil {
log.Printf("[ERR] Error setting placement_constraints for (%s): %s", d.Id(), err)
}
return nil
}
func flattenServicePlacementConstraints(pcs []*ecs.PlacementConstraint) []map[string]interface{} {
if len(pcs) == 0 {
return nil
}
results := make([]map[string]interface{}, 0)
for _, pc := range pcs {
c := make(map[string]interface{})
c["type"] = *pc.Type
c["expression"] = *pc.Expression
results = append(results, c)
}
return results
}
func flattenPlacementStrategy(pss []*ecs.PlacementStrategy) []map[string]interface{} {
if len(pss) == 0 {
return nil

View File

@ -13,52 +13,52 @@ import (
func TestParseTaskDefinition(t *testing.T) {
cases := map[string]map[string]interface{}{
"invalid": map[string]interface{}{
"invalid": {
"family": "",
"revision": "",
"isValid": false,
},
"invalidWithColon:": map[string]interface{}{
"invalidWithColon:": {
"family": "",
"revision": "",
"isValid": false,
},
"1234": map[string]interface{}{
"1234": {
"family": "",
"revision": "",
"isValid": false,
},
"invalid:aaa": map[string]interface{}{
"invalid:aaa": {
"family": "",
"revision": "",
"isValid": false,
},
"invalid=family:1": map[string]interface{}{
"invalid=family:1": {
"family": "",
"revision": "",
"isValid": false,
},
"invalid:name:1": map[string]interface{}{
"invalid:name:1": {
"family": "",
"revision": "",
"isValid": false,
},
"valid:1": map[string]interface{}{
"valid:1": {
"family": "valid",
"revision": "1",
"isValid": true,
},
"abc12-def:54": map[string]interface{}{
"abc12-def:54": {
"family": "abc12-def",
"revision": "54",
"isValid": true,
},
"lorem_ip-sum:123": map[string]interface{}{
"lorem_ip-sum:123": {
"family": "lorem_ip-sum",
"revision": "123",
"isValid": true,
},
"lorem-ipsum:1": map[string]interface{}{
"lorem-ipsum:1": {
"family": "lorem-ipsum",
"revision": "1",
"isValid": true,
@ -89,14 +89,14 @@ func TestAccAWSEcsServiceWithARN(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsService,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"),
),
},
resource.TestStep{
{
Config: testAccAWSEcsServiceModified,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"),
@ -112,14 +112,14 @@ func TestAccAWSEcsServiceWithFamilyAndRevision(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsServiceWithFamilyAndRevision,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.jenkins"),
),
},
resource.TestStep{
{
Config: testAccAWSEcsServiceWithFamilyAndRevisionModified,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.jenkins"),
@ -141,7 +141,7 @@ func TestAccAWSEcsServiceWithRenamedCluster(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsServiceWithRenamedCluster,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.ghost"),
@ -150,7 +150,7 @@ func TestAccAWSEcsServiceWithRenamedCluster(t *testing.T) {
),
},
resource.TestStep{
{
Config: testAccAWSEcsServiceWithRenamedClusterModified,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.ghost"),
@ -168,7 +168,7 @@ func TestAccAWSEcsService_withIamRole(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsService_withIamRole,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.ghost"),
@ -184,7 +184,7 @@ func TestAccAWSEcsService_withDeploymentValues(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsServiceWithDeploymentValues,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"),
@ -205,13 +205,13 @@ func TestAccAWSEcsService_withLbChanges(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsService_withLbChanges,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.with_lb_changes"),
),
},
resource.TestStep{
{
Config: testAccAWSEcsService_withLbChanges_modified,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.with_lb_changes"),
@ -229,7 +229,7 @@ func TestAccAWSEcsService_withEcsClusterName(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsServiceWithEcsClusterName,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.jenkins"),
@ -247,7 +247,7 @@ func TestAccAWSEcsService_withAlb(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsServiceWithAlb,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.with_alb"),
@ -263,14 +263,14 @@ func TestAccAWSEcsServiceWithPlacementStrategy(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsService,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"),
resource.TestCheckResourceAttr("aws_ecs_service.mongo", "placement_strategy.#", "0"),
),
},
resource.TestStep{
{
Config: testAccAWSEcsServiceWithPlacementStrategy,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"),
@ -281,6 +281,23 @@ func TestAccAWSEcsServiceWithPlacementStrategy(t *testing.T) {
})
}
func TestAccAWSEcsServiceWithPlacementConstraints(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSEcsServiceWithPlacementConstraint,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.mongo"),
resource.TestCheckResourceAttr("aws_ecs_service.mongo", "placement_constraints.#", "1"),
),
},
},
})
}
func testAccCheckAWSEcsServiceDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ecsconn
@ -416,6 +433,38 @@ resource "aws_ecs_service" "mongo" {
}
`
var testAccAWSEcsServiceWithPlacementConstraint = `
resource "aws_ecs_cluster" "default" {
name = "terraformecstest21"
}
resource "aws_ecs_task_definition" "mongo" {
family = "mongodb"
container_definitions = <<DEFINITION
[
{
"cpu": 128,
"essential": true,
"image": "mongo:latest",
"memory": 128,
"name": "mongodb"
}
]
DEFINITION
}
resource "aws_ecs_service" "mongo" {
name = "mongodb"
cluster = "${aws_ecs_cluster.default.id}"
task_definition = "${aws_ecs_task_definition.mongo.arn}"
desired_count = 1
placement_constraints {
type = "memberOf"
expression = "attribute:ecs.availability-zone in [us-west-2a, us-west-2b]"
}
}
`
var testAccAWSEcsService_withIamRole = `
resource "aws_ecs_cluster" "main" {
name = "terraformecstest11"

View File

@ -21,23 +21,23 @@ func resourceAwsEcsTaskDefinition() *schema.Resource {
Delete: resourceAwsEcsTaskDefinitionDelete,
Schema: map[string]*schema.Schema{
"arn": &schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"family": &schema.Schema{
"family": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"revision": &schema.Schema{
"revision": {
Type: schema.TypeInt,
Computed: true,
},
"container_definitions": &schema.Schema{
"container_definitions": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
@ -47,13 +47,13 @@ func resourceAwsEcsTaskDefinition() *schema.Resource {
},
},
"task_role_arn": &schema.Schema{
"task_role_arn": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"network_mode": &schema.Schema{
"network_mode": {
Type: schema.TypeString,
Optional: true,
Computed: true,
@ -61,18 +61,18 @@ func resourceAwsEcsTaskDefinition() *schema.Resource {
ValidateFunc: validateAwsEcsTaskDefinitionNetworkMode,
},
"volume": &schema.Schema{
"volume": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"host_path": &schema.Schema{
"host_path": {
Type: schema.TypeString,
Optional: true,
},
@ -81,19 +81,19 @@ func resourceAwsEcsTaskDefinition() *schema.Resource {
Set: resourceAwsEcsTaskDefinitionVolumeHash,
},
"placement_constraints": &schema.Schema{
"placement_constraints": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
MaxItems: 10,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": &schema.Schema{
"type": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
"expression": &schema.Schema{
"expression": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
@ -108,9 +108,9 @@ func resourceAwsEcsTaskDefinition() *schema.Resource {
func validateAwsEcsTaskDefinitionNetworkMode(v interface{}, k string) (ws []string, errors []error) {
value := strings.ToLower(v.(string))
validTypes := map[string]struct{}{
"bridge": struct{}{},
"host": struct{}{},
"none": struct{}{},
"bridge": {},
"host": {},
"none": {},
}
if _, ok := validTypes[value]; !ok {

View File

@ -17,13 +17,13 @@ func TestAccAWSEcsTaskDefinition_basic(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsTaskDefinitionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsTaskDefinition,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsTaskDefinitionExists("aws_ecs_task_definition.jenkins", &def),
),
},
resource.TestStep{
{
Config: testAccAWSEcsTaskDefinitionModified,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsTaskDefinitionExists("aws_ecs_task_definition.jenkins", &def),
@ -41,7 +41,7 @@ func TestAccAWSEcsTaskDefinition_withScratchVolume(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsTaskDefinitionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsTaskDefinitionWithScratchVolume,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsTaskDefinitionExists("aws_ecs_task_definition.sleep", &def),
@ -59,14 +59,14 @@ func TestAccAWSEcsTaskDefinition_withEcsService(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsTaskDefinitionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsTaskDefinitionWithEcsService,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsTaskDefinitionExists("aws_ecs_task_definition.sleep", &def),
testAccCheckAWSEcsServiceExists("aws_ecs_service.sleep-svc"),
),
},
resource.TestStep{
{
Config: testAccAWSEcsTaskDefinitionWithEcsServiceModified,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsTaskDefinitionExists("aws_ecs_task_definition.sleep", &def),
@ -84,7 +84,7 @@ func TestAccAWSEcsTaskDefinition_withTaskRoleArn(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsTaskDefinitionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsTaskDefinitionWithTaskRoleArn,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsTaskDefinitionExists("aws_ecs_task_definition.sleep", &def),
@ -101,7 +101,7 @@ func TestAccAWSEcsTaskDefinition_withNetworkMode(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsTaskDefinitionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsTaskDefinitionWithNetworkMode,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsTaskDefinitionExists("aws_ecs_task_definition.sleep", &def),
@ -120,7 +120,7 @@ func TestAccAWSEcsTaskDefinition_constraint(t *testing.T) {
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsTaskDefinitionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccAWSEcsTaskDefinition_constraint,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsTaskDefinitionExists("aws_ecs_task_definition.jenkins", &def),

View File

@ -35,6 +35,11 @@ resource "aws_ecs_service" "mongo" {
container_name = "mongo"
container_port = 8080
}
placement_constraints {
type = "memberOf"
expression = "attribute:ecs.availability-zone in [us-west-2a, us-west-2b]"
}
}
```
@ -54,6 +59,8 @@ into consideration during task placement. The maximum number of
`placement_strategy` blocks is `5`. See [the related docs](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-placement-strategies.html) for
details on attributes.
* `load_balancer` - (Optional) A load balancer block. Load balancers documented below.
* `placement_constraints` - (Optional) rules that are taken into consideration during task placement. Maximum number of
`placement_constraints` is `10`. Defined below.
-> **Note:** As a result of an AWS limitation, a single `load_balancer` can be attached to the ECS service at most. See [related docs](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-load-balancing.html#load-balancing-concepts).
@ -64,6 +71,16 @@ Load balancers support the following:
* `container_name` - (Required) The name of the container to associate with the load balancer (as it appears in a container definition).
* `container_port` - (Required) The port on the container to associate with the load balancer.
## placement_constraints
`placement_constraints` support the following:
* `expression` - Cluster Query Language expression to apply to the constraint.
For more information, see [Cluster Query Language in the Amazon EC2 Container
Service Developer
Guide](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-query-language.html).
* `type` - The type of constraint. The only valid values at this time are `memberOf` and `distinctInstance`.
## Attributes Reference