Skip to content

Commit 1b79e4b

Browse files
author
Shaun Verch
committed
Add support for a custom aws endpoint
Fixes #494. This makes it possible to run terratest against a custom aws endpoint. This allows it to be used woth [Moto's standalone server mode](http://docs.getmoto.org/en/latest/docs/server_mode.html) for example, to test AWS modules locally without needing an AWS account or any access to AWS. Unfortunately these tests don't pass as is, because they would require setting up the moto server, and I'm not sure where that setup should be added. They do pass if the moto server is running.
1 parent e0c790c commit 1b79e4b

File tree

6 files changed

+320
-5
lines changed

6 files changed

+320
-5
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Terraform AWS S3 Example
2+
3+
This folder contains a simple Terraform module to demonstrate using custom
4+
endpoints. It's deploying some AWS resources to `http://localhost:5000`, which
5+
is the default port for [moto running in server
6+
mode](http://docs.getmoto.org/en/latest/docs/server_mode.html). This allows for
7+
testing terraform modules locally with no connection to AWS.
8+
9+
Check out
10+
[test/terraform_aws_endpoint_example_test.go](/test/terraform_aws_endpoint_example_test.go)
11+
to see how you can write automated tests for this module.
12+
13+
## Running this module manually
14+
15+
1. Run [Moto locally in server mode](http://docs.getmoto.org/en/latest/docs/server_mode.html)
16+
1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.
17+
1. Run `terraform init`.
18+
1. Run `terraform apply`.
19+
1. When you're done, run `terraform destroy`.
20+
21+
## Running automated tests against this module
22+
23+
1. Run [Moto locally in server mode](http://docs.getmoto.org/en/latest/docs/server_mode.html)
24+
1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.
25+
1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.
26+
1. `cd test`
27+
1. `dep ensure`
28+
1. `go test -v -run TestTerraformAwsEndpointExample`
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# ---------------------------------------------------------------------------------------------------------------------
2+
# PIN TERRAFORM VERSION TO >= 0.12
3+
# The examples have been upgraded to 0.12 syntax
4+
# ---------------------------------------------------------------------------------------------------------------------
5+
provider "aws" {
6+
region = var.region
7+
access_key = "dummy"
8+
secret_key = "dummy"
9+
10+
endpoints {
11+
sts = "http://localhost:5000"
12+
s3 = "http://localhost:5000"
13+
}
14+
}
15+
16+
terraform {
17+
# This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting
18+
# 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it
19+
# forwards compatible with 0.13.x code.
20+
required_version = ">= 0.12.26"
21+
}
22+
23+
# ---------------------------------------------------------------------------------------------------------------------
24+
# DEPLOY A S3 BUCKET WITH VERSIONING ENABLED INCLUDING TAGS TO A LOCAL ENDPOINT
25+
# See test/terraform_aws_endpoint_example_test.go for how to write automated tests for this code.
26+
# ---------------------------------------------------------------------------------------------------------------------
27+
28+
# Deploy and configure test S3 bucket with versioning and access log
29+
resource "aws_s3_bucket" "test_bucket" {
30+
bucket = "${local.aws_account_id}-${var.tag_bucket_name}"
31+
32+
tags = {
33+
Name = var.tag_bucket_name
34+
Environment = var.tag_bucket_environment
35+
}
36+
}
37+
38+
resource "aws_s3_bucket_logging" "test_bucket" {
39+
bucket = aws_s3_bucket.test_bucket.id
40+
target_bucket = aws_s3_bucket.test_bucket_logs.id
41+
target_prefix = "TFStateLogs/"
42+
}
43+
44+
resource "aws_s3_bucket_versioning" "test_bucket" {
45+
bucket = aws_s3_bucket.test_bucket.id
46+
versioning_configuration {
47+
status = "Enabled"
48+
}
49+
}
50+
51+
resource "aws_s3_bucket_acl" "test_bucket" {
52+
bucket = aws_s3_bucket.test_bucket.id
53+
acl = "private"
54+
}
55+
56+
57+
# Deploy S3 bucket to collect access logs for test bucket
58+
resource "aws_s3_bucket" "test_bucket_logs" {
59+
bucket = "${local.aws_account_id}-${var.tag_bucket_name}-logs"
60+
61+
tags = {
62+
Name = "${local.aws_account_id}-${var.tag_bucket_name}-logs"
63+
Environment = var.tag_bucket_environment
64+
}
65+
66+
force_destroy = true
67+
}
68+
69+
resource "aws_s3_bucket_acl" "test_bucket_logs" {
70+
bucket = aws_s3_bucket.test_bucket_logs.id
71+
acl = "log-delivery-write"
72+
}
73+
74+
# Configure bucket access policies
75+
76+
resource "aws_s3_bucket_policy" "bucket_access_policy" {
77+
count = var.with_policy ? 1 : 0
78+
bucket = aws_s3_bucket.test_bucket.id
79+
policy = data.aws_iam_policy_document.s3_bucket_policy.json
80+
}
81+
82+
data "aws_iam_policy_document" "s3_bucket_policy" {
83+
statement {
84+
effect = "Allow"
85+
principals {
86+
# TF-UPGRADE-TODO: In Terraform v0.10 and earlier, it was sometimes necessary to
87+
# force an interpolation expression to be interpreted as a list by wrapping it
88+
# in an extra set of list brackets. That form was supported for compatibility in
89+
# v0.11, but is no longer supported in Terraform v0.12.
90+
#
91+
# If the expression in the following list itself returns a list, remove the
92+
# brackets to avoid interpretation as a list of lists. If the expression
93+
# returns a single list item then leave it as-is and remove this TODO comment.
94+
identifiers = [local.aws_account_id]
95+
type = "AWS"
96+
}
97+
actions = ["*"]
98+
resources = ["${aws_s3_bucket.test_bucket.arn}/*"]
99+
}
100+
101+
statement {
102+
effect = "Deny"
103+
principals {
104+
identifiers = ["*"]
105+
type = "AWS"
106+
}
107+
actions = ["*"]
108+
resources = ["${aws_s3_bucket.test_bucket.arn}/*"]
109+
110+
condition {
111+
test = "Bool"
112+
variable = "aws:SecureTransport"
113+
values = [
114+
"false",
115+
]
116+
}
117+
}
118+
}
119+
120+
# ---------------------------------------------------------------------------------------------------------------------
121+
# LOCALS
122+
# Used to represent any data that requires complex expressions/interpolations
123+
# ---------------------------------------------------------------------------------------------------------------------
124+
125+
data "aws_caller_identity" "current" {
126+
}
127+
128+
locals {
129+
aws_account_id = data.aws_caller_identity.current.account_id
130+
}
131+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
output "bucket_id" {
2+
value = aws_s3_bucket.test_bucket.id
3+
}
4+
5+
output "bucket_arn" {
6+
value = aws_s3_bucket.test_bucket.arn
7+
}
8+
9+
output "logging_target_bucket" {
10+
value = aws_s3_bucket_logging.test_bucket.target_bucket
11+
}
12+
13+
output "logging_target_prefix" {
14+
value = aws_s3_bucket_logging.test_bucket.target_prefix
15+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# ---------------------------------------------------------------------------------------------------------------------
2+
# ENVIRONMENT VARIABLES
3+
# Define these secrets as environment variables
4+
# ---------------------------------------------------------------------------------------------------------------------
5+
6+
# AWS_ACCESS_KEY_ID
7+
# AWS_SECRET_ACCESS_KEY
8+
9+
# ---------------------------------------------------------------------------------------------------------------------
10+
# REQUIRED PARAMETERS
11+
# You must provide a value for each of these parameters.
12+
# ---------------------------------------------------------------------------------------------------------------------
13+
variable "region" {
14+
description = "The AWS region to deploy to"
15+
type = string
16+
}
17+
18+
# ---------------------------------------------------------------------------------------------------------------------
19+
# OPTIONAL PARAMETERS
20+
# These parameters have reasonable defaults.
21+
# ---------------------------------------------------------------------------------------------------------------------
22+
23+
variable "with_policy" {
24+
description = "If set to `true`, the bucket will be created with a bucket policy."
25+
type = bool
26+
default = false
27+
}
28+
29+
variable "tag_bucket_name" {
30+
description = "The Name tag to set for the S3 Bucket."
31+
type = string
32+
default = "Test Bucket"
33+
}
34+
35+
variable "tag_bucket_environment" {
36+
description = "The Environment tag to set for the S3 Bucket."
37+
type = string
38+
default = "Test"
39+
}
40+

modules/aws/auth.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
const (
1818
AuthAssumeRoleEnvVar = "TERRATEST_IAM_ROLE" // OS environment variable name through which Assume Role ARN may be passed for authentication
19+
CustomEndpointEnvVar = "TERRATEST_CUSTOM_AWS_ENDPOINT" // Custom endpoint to use as aws service
1920
)
2021

2122
// NewAuthenticatedSession creates an AWS session following to standard AWS authentication workflow.
@@ -32,6 +33,11 @@ func NewAuthenticatedSession(region string) (*session.Session, error) {
3233
func NewAuthenticatedSessionFromDefaultCredentials(region string) (*session.Session, error) {
3334
awsConfig := aws.NewConfig().WithRegion(region)
3435

36+
if customEndpoint, ok := os.LookupEnv(CustomEndpointEnvVar); ok {
37+
awsConfig.WithEndpoint(customEndpoint)
38+
}
39+
awsConfig.WithEndpoint(os.Getenv(CustomEndpointEnvVar))
40+
3541
sessionOptions := session.Options{
3642
Config: *awsConfig,
3743
SharedConfigState: session.SharedConfigEnable,
@@ -68,7 +74,13 @@ func NewAuthenticatedSessionFromRole(region string, roleARN string) (*session.Se
6874
// CreateAwsSessionFromRole returns a new AWS session after assuming the role
6975
// whose ARN is provided in roleARN.
7076
func CreateAwsSessionFromRole(region string, roleARN string) (*session.Session, error) {
71-
sess, err := session.NewSession(aws.NewConfig().WithRegion(region))
77+
awsConfig := aws.NewConfig().WithRegion(region)
78+
79+
if customEndpoint, ok := os.LookupEnv(CustomEndpointEnvVar); ok {
80+
awsConfig.WithEndpoint(customEndpoint)
81+
}
82+
83+
sess, err := session.NewSession(awsConfig)
7284
if err != nil {
7385
return nil, err
7486
}
@@ -86,8 +98,15 @@ func AssumeRole(sess *session.Session, roleARN string) *session.Session {
8698
// CreateAwsSessionWithCreds creates a new AWS session using explicit credentials. This is useful if you want to create an IAM User dynamically and
8799
// create an AWS session authenticated as the new IAM User.
88100
func CreateAwsSessionWithCreds(region string, accessKeyID string, secretAccessKey string) (*session.Session, error) {
89-
creds := CreateAwsCredentials(accessKeyID, secretAccessKey)
90-
return session.NewSession(aws.NewConfig().WithRegion(region).WithCredentials(creds))
101+
awsConfig := aws.NewConfig().WithRegion(region)
102+
103+
if customEndpoint, ok := os.LookupEnv(CustomEndpointEnvVar); ok {
104+
awsConfig.WithEndpoint(customEndpoint)
105+
}
106+
107+
awsConfig.WithCredentials(CreateAwsCredentials(accessKeyID, secretAccessKey))
108+
109+
return session.NewSession(awsConfig)
91110
}
92111

93112
// CreateAwsSessionWithMfa creates a new AWS session authenticated using an MFA token retrieved using the given STS client and MFA Device.
@@ -109,8 +128,15 @@ func CreateAwsSessionWithMfa(region string, stsClient *sts.STS, mfaDevice *iam.V
109128
secretAccessKey := *output.Credentials.SecretAccessKey
110129
sessionToken := *output.Credentials.SessionToken
111130

112-
creds := CreateAwsCredentialsWithSessionToken(accessKeyID, secretAccessKey, sessionToken)
113-
return session.NewSession(aws.NewConfig().WithRegion(region).WithCredentials(creds))
131+
awsConfig := aws.NewConfig().WithRegion(region)
132+
133+
if customEndpoint, ok := os.LookupEnv(CustomEndpointEnvVar); ok {
134+
awsConfig.WithEndpoint(customEndpoint)
135+
}
136+
137+
awsConfig.WithCredentials(CreateAwsCredentialsWithSessionToken(accessKeyID, secretAccessKey, sessionToken))
138+
139+
return session.NewSession(awsConfig)
114140
}
115141

116142
// CreateAwsCredentials creates an AWS Credentials configuration with specific AWS credentials.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package test
2+
3+
import (
4+
"os"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
9+
"github.com/gruntwork-io/terratest/modules/aws"
10+
"github.com/gruntwork-io/terratest/modules/random"
11+
"github.com/gruntwork-io/terratest/modules/terraform"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
// An example of how to test the Terraform module in examples/terraform-aws-endpoint-example using Terratest.
16+
func TestTerraformAwsEndpointExample(t *testing.T) {
17+
t.Parallel()
18+
19+
// Set a custom endpoint for AWS, and set the keys to dummy keys to
20+
// pass that check
21+
os.Setenv("TERRATEST_CUSTOM_AWS_ENDPOINT", "http://localhost:5000")
22+
os.Setenv("AWS_ACCESS_KEY_ID", "dummy")
23+
os.Setenv("AWS_SECRET_ACCESS_KEY", "dummy")
24+
25+
// Give this S3 Bucket a unique ID for a name tag so we can distinguish it from any other Buckets provisioned
26+
// in your AWS account
27+
expectedName := fmt.Sprintf("terratest-aws-endpoint-example-%s", strings.ToLower(random.UniqueId()))
28+
29+
// Give this S3 Bucket an environment to operate as a part of for the purposes of resource tagging
30+
expectedEnvironment := "Automated Testing"
31+
32+
// Pick a random AWS region to test in. This helps ensure your code works in all regions.
33+
awsRegion := aws.GetRandomStableRegion(t, nil, nil)
34+
35+
// Construct the terraform options with default retryable errors to handle the most common retryable errors in
36+
// terraform testing.
37+
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
38+
// The path to where our Terraform code is located
39+
TerraformDir: "../examples/terraform-aws-endpoint-example",
40+
41+
// Variables to pass to our Terraform code using -var options
42+
Vars: map[string]interface{}{
43+
"tag_bucket_name": expectedName,
44+
"tag_bucket_environment": expectedEnvironment,
45+
"with_policy": "true",
46+
"region": awsRegion,
47+
},
48+
})
49+
50+
// At the end of the test, run `terraform destroy` to clean up any resources that were created
51+
defer terraform.Destroy(t, terraformOptions)
52+
53+
// This will run `terraform init` and `terraform apply` and fail the test if there are any errors
54+
terraform.InitAndApply(t, terraformOptions)
55+
56+
// Run `terraform output` to get the value of an output variable
57+
bucketID := terraform.Output(t, terraformOptions, "bucket_id")
58+
59+
// Verify that our Bucket has versioning enabled
60+
actualStatus := aws.GetS3BucketVersioning(t, awsRegion, bucketID)
61+
expectedStatus := "Enabled"
62+
assert.Equal(t, expectedStatus, actualStatus)
63+
64+
// Verify that our Bucket has a policy attached
65+
aws.AssertS3BucketPolicyExists(t, awsRegion, bucketID)
66+
67+
// Verify that our bucket has server access logging TargetBucket set to what's expected
68+
loggingTargetBucket := aws.GetS3BucketLoggingTarget(t, awsRegion, bucketID)
69+
expectedLogsTargetBucket := fmt.Sprintf("%s-logs", bucketID)
70+
loggingObjectTargetPrefix := aws.GetS3BucketLoggingTargetPrefix(t, awsRegion, bucketID)
71+
expectedLogsTargetPrefix := "TFStateLogs/"
72+
73+
assert.Equal(t, expectedLogsTargetBucket, loggingTargetBucket)
74+
assert.Equal(t, expectedLogsTargetPrefix, loggingObjectTargetPrefix)
75+
}

0 commit comments

Comments
 (0)