├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── SUPPORT ├── examples ├── 1_nic_with_new_vpc │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── 2_nic_with_new_vpc │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ └── variables.tf └── 3_nic_with_new_vpc │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── f5_onboard.tmpl ├── iam.tf ├── main.tf ├── outputs.tf ├── tests ├── 1_nic_with_new_vpc_test.go ├── 3_nic_with_new_vpc_test.go └── Makefile ├── variables.tf └── versions /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # .tfvars files 9 | *.tfvars 10 | 11 | # Terraform crash log 12 | **/crash.log 13 | 14 | # Go Tests 15 | **/vendor 16 | **/Gopkg.toml 17 | **/Gopkg.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - master 6 | 7 | branches: 8 | only: 9 | - master 10 | 11 | env: 12 | - TF_INPUT=false TF_IN_AUTOMATION=true 13 | 14 | # Install terraform 15 | before_install: 16 | - curl -sLo /tmp/terraform.zip https://releases.hashicorp.com/terraform/0.12.9/terraform_0.12.9_linux_amd64.zip 17 | - unzip /tmp/terraform.zip -d /tmp 18 | - mkdir -p ~/bin 19 | - mv /tmp/terraform ~/bin 20 | - export PATH="~/bin:$PATH" 21 | 22 | script: 23 | - go test -v -count=1 -timeout 30m ./... -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 F5 Networks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation Notice 2 | This module is no longer under active development. 3 | For new projects please use the [officially support F5 BIG-IP module](github.com/F5Networks/terraform-aws-bigip-module). 4 | For existing projects, there is currently no planned date for retiring this module. However, it is highly recommended that you plan to migrate to the [officially support F5 BIG-IP module](github.com/F5Networks/terraform-aws-bigip-module). 5 | 6 | # AWS BIG-IP Terraform Module 7 | Terraform module to deploy an F5 BIG-IP in AWS. This module currently supports 1 and 3 nic deployments and defaults to using the AWS Marketplace PAYG (pay-as-you-go) 200Mbps BEST license. If you would like to use a bring your own license (BYOL) AMI the set the *f5_ami_search_name* variable accordingly. 8 | 9 | **NOTE:** You will need to accept the AWS Marketplace offer for your selected BIG-IP AMI. 10 | **NOTE:** This code is provided for demonstration purposes and is not intended to be used for production deployments. 11 | 12 | ## Password Policy (New in 0.1.2) 13 | For security reasons the module no longer generates a random password that is stored in the Terraform state file. Instead, a password must be created in the AWS Secrets Manager and the Secret name must be supplied to the module. For demonstration purposes, the examples show how to do this using an randomly generated password. 14 | 15 | ## Dependencies 16 | This module requires that the user has created a password and stored it in the AWS Secret Manager before calling the module. For information on how to do this please refer to the [AWS Secret Manager docs](https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html). 17 | 18 | ## Terraform Version 19 | This modules supports Terraform 0.12 and higher 20 | 21 | ## Examples 22 | We have provided some common deployment examples below. However, if you would like to see full end-to-end examples with the creation of all required objects check out the [examples](https://github.com/f5devcentral/terraform-aws-bigip/tree/master/examples) folder in the [GitHub repository](https://github.com/f5devcentral/terraform-aws-bigip/). 23 | 24 | ### Example 1-NIC Deployment PAYG 25 | ```hcl 26 | module bigip { 27 | source = "f5devcentral/bigip/aws" 28 | version = "0.1.2" 29 | 30 | prefix = "bigip" 31 | f5_instance_count = 1 32 | ec2_key_name = "my-key" 33 | aws_secretmanager_secret_id = "my_bigip_password" 34 | mgmt_subnet_security_group_ids = [sg-01234567890abcdef] 35 | vpc_mgmt_subnet_ids = [subnet-01234567890abcdef] 36 | } 37 | ``` 38 | ### Example 1-NIC Deployment BYOL 39 | ```hcl 40 | module bigip { 41 | source = "f5devcentral/bigip/aws" 42 | version = "0.1.2" 43 | 44 | prefix = "bigip" 45 | f5_instance_count = 1 46 | ec2_key_name = "my-key" 47 | aws_secretmanager_secret_id = "my_bigip_password" 48 | mgmt_subnet_security_group_ids = [sg-01234567890abcdef] 49 | vpc_mgmt_subnet_ids = [subnet-01234567890abcdef] 50 | f5_ami_search_name = "F5 Networks BIGIP-14.0.1*BYOL*All Modules 1 Boot*" 51 | } 52 | ``` 53 | 54 | ### Example 2-NIC Deployment PAYG 55 | ```hcl 56 | module bigip { 57 | source = "f5devcentral/bigip/aws" 58 | version = "0.1.2" 59 | 60 | prefix = "bigip" 61 | f5_instance_count = 1 62 | ec2_key_name = "my-key" 63 | aws_secretmanager_secret_id = "my_bigip_password" 64 | mgmt_subnet_security_group_ids = [sg-01234567890abcdef] 65 | public_subnet_security_group_ids = [sg-01234567890ghijkl] 66 | vpc_mgmt_subnet_ids = [subnet-01234567890abcdef] 67 | vpc_public_subnet_ids = [subnet-01234567890ghijkl] 68 | } 69 | ``` 70 | 71 | ### Example 3-NIC Deployment PAYG 72 | ```hcl 73 | module bigip { 74 | source = "f5devcentral/bigip/aws" 75 | version = "0.1.2" 76 | 77 | prefix = "bigip" 78 | f5_instance_count = 1 79 | ec2_key_name = "my-key" 80 | aws_secretmanager_secret_id = "my_bigip_password" 81 | mgmt_subnet_security_group_ids = [sg-01234567890abcdef] 82 | public_subnet_security_group_ids = [sg-01234567890ghijkl] 83 | private_subnet_security_group_ids = [sg-01234567890mnopqr] 84 | vpc_mgmt_subnet_ids = [subnet-01234567890abcdef] 85 | vpc_public_subnet_ids = [subnet-01234567890ghijkl] 86 | vpc_private_subnet_ids = [subnet-01234567890mnopqr] 87 | } 88 | ``` 89 | 90 | ## Finding AWS Machine Images (AMI) 91 | If there is a specific F5 BIG-IP AMI you would like to use you can update the f5_ami_search_name variable to reflect the AMI name or name pattern you are looking for. 92 | 93 | Example to find F5 AMIs for PAYG 200Mbps BEST licensing: 94 | ```bash 95 | aws ec2 describe-images --owners 679593333241 --filters "Name=name, Values=F5 Networks BIGIP-14.0.1-0.0.14* PAYG - Best 200Mbps*" 96 | ``` 97 | 98 | Example to find F5 AMIs for BYOL 200Mbps BEST licensing: 99 | ```bash 100 | aws ec2 describe-images --owners 679593333241 --filters "Name=name, Values=F5 Networks BIGIP-14.0.1*BYOL*All Modules 1 Boot*" 101 | ``` 102 | -------------------------------------------------------------------------------- /SUPPORT: -------------------------------------------------------------------------------- 1 | Maintenance and F5 Technical Support of the F5 code is provided only if the software (i) is unmodified; and (ii) has been marked as F5 Supported in SOL80012344, (https://support.f5.com/csp/article/K80012344). Support will only be provided to customers who have an existing support contract, purchased separately, subject to F5’s support policies available at http://www.f5.com/about/guidelines-policies/ and http://askf5.com. 2 | 3 | The information required when opening a support case is available at https://support.f5.com/csp/article/K60974137 4 | -------------------------------------------------------------------------------- /examples/1_nic_with_new_vpc/README.md: -------------------------------------------------------------------------------- 1 | # New VPC with a 1-nic BIG-IP in each AZ 2 | This examples deploys a new VPC with subnets across 2 availability zones. It also builds a 1-nic BIG-IP in each availability zone. 3 | 4 | **Note:** This example creates a random, temporary password for the BIG-IP which is stored in the Terraform state file. This is not a good practice for production environments. Ideally, you would use a random password generated by the AWS Secrets Manager. 5 | 6 | ## Usage 7 | To run this example run the following commands: 8 | ```bash 9 | terraform init 10 | terraform plan 11 | terraform apply --auto-approve 12 | ``` 13 | 14 | **Note:** this examples deploys resources that will cost money. Please run the following command to destroy your environment when finished: 15 | ```bash 16 | terraform destroy --auto-approve 17 | ``` -------------------------------------------------------------------------------- /examples/1_nic_with_new_vpc/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.region 3 | # access_key = var.AccessKeyID 4 | # secret_key = var.SecretAccessKey 5 | } 6 | 7 | # 8 | # Create a random id 9 | # 10 | resource "random_id" "id" { 11 | byte_length = 2 12 | } 13 | 14 | # 15 | # Create random password for BIG-IP 16 | # 17 | resource "random_password" "password" { 18 | length = 16 19 | special = true 20 | override_special = " #%*+,-./:=?@[]^_~" 21 | } 22 | 23 | # 24 | # Create Secret Store and Store BIG-IP Password 25 | # 26 | resource "aws_secretsmanager_secret" "bigip" { 27 | name = format("%s-bigip-secret-%s", var.prefix, random_id.id.hex) 28 | } 29 | resource "aws_secretsmanager_secret_version" "bigip-pwd" { 30 | secret_id = aws_secretsmanager_secret.bigip.id 31 | secret_string = random_password.password.result 32 | } 33 | 34 | # 35 | # Create the VPC 36 | # 37 | module "vpc" { 38 | source = "terraform-aws-modules/vpc/aws" 39 | 40 | name = format("%s-vpc-%s", local.prefix, random_id.id.hex) 41 | cidr = local.cidr 42 | enable_dns_hostnames = true 43 | enable_dns_support = true 44 | 45 | azs = local.azs 46 | 47 | public_subnets = [ 48 | for num in range(length(local.azs)) : 49 | cidrsubnet(local.cidr, 8, num) 50 | ] 51 | 52 | tags = { 53 | Name = format("%s-vpc-%s", local.prefix, random_id.id.hex) 54 | Terraform = "true" 55 | Environment = "dev" 56 | } 57 | } 58 | 59 | # 60 | # Create a security group for port 80 traffic 61 | # 62 | module "web_server_sg" { 63 | source = "terraform-aws-modules/security-group/aws//modules/http-80" 64 | 65 | name = format("%s-web-server-%s", local.prefix, random_id.id.hex) 66 | description = "Security group for web-server with HTTP ports" 67 | vpc_id = module.vpc.vpc_id 68 | 69 | ingress_cidr_blocks = [local.allowed_app_cidr] 70 | } 71 | 72 | # 73 | # Create a security group for port 443 traffic 74 | # 75 | module "web_server_secure_sg" { 76 | source = "terraform-aws-modules/security-group/aws//modules/https-443" 77 | 78 | name = format("%s-web-server-secure-%s", local.prefix, random_id.id.hex) 79 | description = "Security group for web-server with HTTPS ports" 80 | vpc_id = module.vpc.vpc_id 81 | 82 | ingress_cidr_blocks = [local.allowed_app_cidr] 83 | } 84 | 85 | # 86 | # Create a security group for port 8443 traffic 87 | # 88 | module "bigip_mgmt_secure_sg" { 89 | source = "terraform-aws-modules/security-group/aws//modules/https-8443" 90 | 91 | name = format("%s-bigip-mgmt-%s", local.prefix, random_id.id.hex) 92 | description = "Security group for BIG-IP MGMT Interface" 93 | vpc_id = module.vpc.vpc_id 94 | 95 | ingress_cidr_blocks = [local.allowed_mgmt_cidr] 96 | } 97 | 98 | # 99 | # Create a security group for SSH traffic 100 | # 101 | module "ssh_secure_sg" { 102 | source = "terraform-aws-modules/security-group/aws//modules/ssh" 103 | 104 | name = format("%s-ssh-%s", local.prefix, random_id.id.hex) 105 | description = "Security group for SSH ports open within VPC" 106 | vpc_id = module.vpc.vpc_id 107 | 108 | ingress_cidr_blocks = [local.allowed_mgmt_cidr] 109 | } 110 | 111 | # 112 | # Create BIG-IP 113 | # 114 | module bigip { 115 | source = "../../" 116 | 117 | prefix = format( 118 | "%s-bigip-1-nic_with_new_vpc-%s", 119 | local.prefix, 120 | random_id.id.hex 121 | ) 122 | f5_instance_count = length(local.azs) 123 | ec2_key_name = var.ec2_key_name 124 | aws_secretmanager_secret_id = aws_secretsmanager_secret.bigip.id 125 | mgmt_subnet_security_group_ids = [ 126 | module.web_server_sg.this_security_group_id, 127 | module.web_server_secure_sg.this_security_group_id, 128 | module.ssh_secure_sg.this_security_group_id, 129 | module.bigip_mgmt_secure_sg.this_security_group_id 130 | ] 131 | vpc_mgmt_subnet_ids = module.vpc.public_subnets 132 | } 133 | 134 | # 135 | # Variables used by this example 136 | # 137 | locals { 138 | prefix = "tf-aws-bigip" 139 | region = "us-east-2" 140 | azs = ["us-east-2a", "us-east-2b"] 141 | cidr = "10.0.0.0/16" 142 | allowed_mgmt_cidr = "0.0.0.0/0" 143 | allowed_app_cidr = "0.0.0.0/0" 144 | } 145 | -------------------------------------------------------------------------------- /examples/1_nic_with_new_vpc/outputs.tf: -------------------------------------------------------------------------------- 1 | # VPC 2 | output "vpc_id" { 3 | value = module.vpc.vpc_id 4 | } 5 | 6 | # BIG-IP Management Public IP Addresses 7 | output "bigip_mgmt_ips" { 8 | value = module.bigip.mgmt_public_ips 9 | } 10 | 11 | # BIG-IP Management Public DNS Address 12 | output "bigip_mgmt_dns" { 13 | value = module.bigip.mgmt_public_dns 14 | } 15 | 16 | # BIG-IP Management Port 17 | output "bigip_mgmt_port" { 18 | value = module.bigip.mgmt_port 19 | } 20 | # BIG-IP Password 21 | output "password" { 22 | value = random_password.password 23 | sensitive = true 24 | } 25 | 26 | # BIG-IP Password Secret name 27 | output "aws_secretmanager_secret_name" { 28 | value = aws_secretsmanager_secret.bigip.name 29 | } 30 | -------------------------------------------------------------------------------- /examples/1_nic_with_new_vpc/variables.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Variable for the EC2 Key 3 | # Set via CLI or via terraform.tfvars file 4 | # 5 | variable "ec2_key_name" { 6 | description = "AWS EC2 Key name for SSH access" 7 | type = string 8 | } 9 | 10 | # variable "AccessKeyID" {} 11 | 12 | # variable "SecretAccessKey" {} 13 | 14 | variable "prefix" { 15 | description = "Prefix for resources created by this module" 16 | type = string 17 | default = "terraform-aws-bigip-1nic" 18 | } 19 | -------------------------------------------------------------------------------- /examples/2_nic_with_new_vpc/README.md: -------------------------------------------------------------------------------- 1 | # New VPC with a 2-nic BIG-IP in each AZ 2 | This examples deploys a new VPC with subnets across 2 availability zones. It also builds a 2-nic BIG-IP in each availability zone. 3 | 4 | **Note:** This example creates a random, temporary password for the BIG-IP which is stored in the Terraform state file. This is not a good practice for production environments. Ideally, you would use a random password generated by the AWS Secrets Manager. 5 | 6 | ## Usage 7 | To run this example run the following commands: 8 | ```bash 9 | terraform init 10 | terraform plan 11 | terraform apply --auto-approve 12 | ``` 13 | 14 | **Note:** this examples deploys resources that will cost money. Please run the following command to destroy your environment when finished: 15 | ```bash 16 | terraform destroy --auto-approve 17 | ``` -------------------------------------------------------------------------------- /examples/2_nic_with_new_vpc/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.region 3 | } 4 | 5 | # 6 | # Create a random id 7 | # 8 | resource "random_id" "id" { 9 | byte_length = 2 10 | } 11 | 12 | # 13 | # Create random password for BIG-IP 14 | # 15 | resource "random_password" "password" { 16 | length = 16 17 | special = true 18 | override_special = " #%*+,-./:=?@[]^_~" 19 | } 20 | 21 | # 22 | # Create Secret Store and Store BIG-IP Password 23 | # 24 | resource "aws_secretsmanager_secret" "bigip" { 25 | name = format("%s-bigip-secret-%s", var.prefix, random_id.id.hex) 26 | } 27 | resource "aws_secretsmanager_secret_version" "bigip-pwd" { 28 | secret_id = aws_secretsmanager_secret.bigip.id 29 | secret_string = random_password.password.result 30 | } 31 | 32 | # 33 | # Create the VPC 34 | # 35 | module "vpc" { 36 | source = "terraform-aws-modules/vpc/aws" 37 | 38 | name = format("%s-vpc-%s", local.prefix, random_id.id.hex) 39 | cidr = local.cidr 40 | enable_dns_hostnames = true 41 | enable_dns_support = true 42 | 43 | azs = local.azs 44 | 45 | public_subnets = [ 46 | for num in range(length(local.azs)) : 47 | cidrsubnet(local.cidr, 8, num) 48 | ] 49 | 50 | # using the database subnet method since it allows a public route 51 | database_subnets = [ 52 | for num in range(length(local.azs)) : 53 | cidrsubnet(local.cidr, 8, num + 10) 54 | ] 55 | create_database_subnet_group = true 56 | create_database_subnet_route_table = true 57 | create_database_internet_gateway_route = true 58 | 59 | tags = { 60 | Name = format("%s-vpc-%s", local.prefix, random_id.id.hex) 61 | Terraform = "true" 62 | Environment = "dev" 63 | } 64 | } 65 | 66 | # 67 | # Create a security group for port 80 traffic 68 | # 69 | module "web_server_sg" { 70 | source = "terraform-aws-modules/security-group/aws//modules/http-80" 71 | 72 | name = format("%s-web-server-%s", local.prefix, random_id.id.hex) 73 | description = "Security group for web-server with HTTP ports" 74 | vpc_id = module.vpc.vpc_id 75 | 76 | ingress_cidr_blocks = [local.allowed_app_cidr] 77 | } 78 | 79 | # 80 | # Create a security group for port 443 traffic 81 | # 82 | module "web_server_secure_sg" { 83 | source = "terraform-aws-modules/security-group/aws//modules/https-443" 84 | 85 | name = format("%s-web-server-secure-%s", local.prefix, random_id.id.hex) 86 | description = "Security group for web-server with HTTPS ports" 87 | vpc_id = module.vpc.vpc_id 88 | 89 | ingress_cidr_blocks = [local.allowed_app_cidr] 90 | } 91 | 92 | # 93 | # Create a security group for SSH traffic 94 | # 95 | module "ssh_secure_sg" { 96 | source = "terraform-aws-modules/security-group/aws//modules/ssh" 97 | 98 | name = format("%s-ssh-%s", local.prefix, random_id.id.hex) 99 | description = "Security group for SSH ports open within VPC" 100 | vpc_id = module.vpc.vpc_id 101 | 102 | ingress_cidr_blocks = [local.allowed_mgmt_cidr] 103 | } 104 | 105 | # 106 | # Create BIG-IP 107 | # 108 | module bigip { 109 | source = "../../" 110 | 111 | prefix = format( 112 | "%s-bigip-2-nic_with_new_vpc-%s", 113 | local.prefix, 114 | random_id.id.hex 115 | ) 116 | f5_instance_count = length(local.azs) 117 | ec2_instance_type = "m5.large" 118 | ec2_key_name = var.ec2_key_name 119 | aws_secretmanager_secret_id = aws_secretsmanager_secret.bigip.id 120 | mgmt_subnet_security_group_ids = [ 121 | module.web_server_secure_sg.this_security_group_id, 122 | module.ssh_secure_sg.this_security_group_id 123 | ] 124 | 125 | public_subnet_security_group_ids = [ 126 | module.web_server_sg.this_security_group_id, 127 | module.web_server_secure_sg.this_security_group_id 128 | ] 129 | 130 | vpc_public_subnet_ids = module.vpc.public_subnets 131 | vpc_mgmt_subnet_ids = module.vpc.database_subnets 132 | } 133 | 134 | # 135 | # Variables used by this example 136 | # 137 | locals { 138 | prefix = "tf-aws-bigip" 139 | region = "us-east-2" 140 | azs = [format("%s%s", local.region, "a"), format("%s%s", local.region, "b")] 141 | cidr = "10.0.0.0/16" 142 | allowed_mgmt_cidr = "0.0.0.0/0" 143 | allowed_app_cidr = "0.0.0.0/0" 144 | } 145 | -------------------------------------------------------------------------------- /examples/2_nic_with_new_vpc/outputs.tf: -------------------------------------------------------------------------------- 1 | # VPC 2 | output "vpc_id" { 3 | value = module.vpc.vpc_id 4 | } 5 | 6 | # BIG-IP Management Public IP Addresses 7 | output "bigip_mgmt_ips" { 8 | value = module.bigip.mgmt_public_ips 9 | } 10 | 11 | # BIG-IP Management Public DNS Address 12 | output "bigip_mgmt_dns" { 13 | value = module.bigip.mgmt_public_dns 14 | } 15 | 16 | # BIG-IP Management Port 17 | output "bigip_mgmt_port" { 18 | value = module.bigip.mgmt_port 19 | } 20 | # BIG-IP Password 21 | output "password" { 22 | value = random_password.password 23 | sensitive = true 24 | } 25 | 26 | # BIG-IP Password Secret name 27 | output "aws_secretmanager_secret_name" { 28 | value = aws_secretsmanager_secret.bigip.name 29 | } 30 | -------------------------------------------------------------------------------- /examples/2_nic_with_new_vpc/variables.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Variable for the EC2 Key 3 | # Set via CLI or via terraform.tfvars file 4 | # 5 | variable "ec2_key_name" { 6 | description = "AWS EC2 Key name for SSH access" 7 | type = string 8 | } 9 | 10 | variable "prefix" { 11 | description = "Prefix for resources created by this module" 12 | type = string 13 | default = "terraform-aws-bigip-2nic" 14 | } 15 | -------------------------------------------------------------------------------- /examples/3_nic_with_new_vpc/README.md: -------------------------------------------------------------------------------- 1 | # New VPC with a 3-nic BIG-IP in each AZ 2 | This examples deploys a new VPC with subnets across 2 availability zones. It also builds a 3-nic BIG-IP in each availability zone. 3 | 4 | **Note:** This example creates a random, temporary password for the BIG-IP which is stored in the Terraform state file. This is not a good practice for production environments. Ideally, you would use a random password generated by the AWS Secrets Manager. 5 | 6 | ## Usage 7 | To run this example run the following commands: 8 | ```bash 9 | terraform init 10 | terraform plan 11 | terraform apply --auto-approve 12 | ``` 13 | 14 | **Note:** this examples deploys resources that will cost money. Please run the following command to destroy your environment when finished: 15 | ```bash 16 | terraform destroy --auto-approve 17 | ``` -------------------------------------------------------------------------------- /examples/3_nic_with_new_vpc/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = local.region 3 | } 4 | 5 | # 6 | # Create a random id 7 | # 8 | resource "random_id" "id" { 9 | byte_length = 2 10 | } 11 | 12 | # 13 | # Create random password for BIG-IP 14 | # 15 | resource "random_password" "password" { 16 | length = 16 17 | special = true 18 | override_special = " #%*+,-./:=?@[]^_~" 19 | } 20 | 21 | # 22 | # Create Secret Store and Store BIG-IP Password 23 | # 24 | resource "aws_secretsmanager_secret" "bigip" { 25 | name = format("%s-bigip-secret-%s", var.prefix, random_id.id.hex) 26 | } 27 | resource "aws_secretsmanager_secret_version" "bigip-pwd" { 28 | secret_id = aws_secretsmanager_secret.bigip.id 29 | secret_string = random_password.password.result 30 | } 31 | 32 | # 33 | # Create the VPC 34 | # 35 | module "vpc" { 36 | source = "terraform-aws-modules/vpc/aws" 37 | 38 | name = format("%s-vpc-%s", local.prefix, random_id.id.hex) 39 | cidr = local.cidr 40 | enable_dns_hostnames = true 41 | enable_dns_support = true 42 | 43 | azs = local.azs 44 | 45 | public_subnets = [ 46 | for num in range(length(local.azs)) : 47 | cidrsubnet(local.cidr, 8, num) 48 | ] 49 | 50 | # using the database subnet method since it allows a public route 51 | database_subnets = [ 52 | for num in range(length(local.azs)) : 53 | cidrsubnet(local.cidr, 8, num + 10) 54 | ] 55 | create_database_subnet_group = true 56 | create_database_subnet_route_table = true 57 | create_database_internet_gateway_route = true 58 | 59 | private_subnets = [ 60 | for num in range(length(local.azs)) : 61 | cidrsubnet(local.cidr, 8, num + 20) 62 | ] 63 | 64 | tags = { 65 | Name = format("%s-vpc-%s", local.prefix, random_id.id.hex) 66 | Terraform = "true" 67 | Environment = "dev" 68 | } 69 | } 70 | 71 | # 72 | # Create a security group for port 80 traffic 73 | # 74 | module "web_server_sg" { 75 | source = "terraform-aws-modules/security-group/aws//modules/http-80" 76 | 77 | name = format("%s-web-server-%s", local.prefix, random_id.id.hex) 78 | description = "Security group for web-server with HTTP ports" 79 | vpc_id = module.vpc.vpc_id 80 | 81 | ingress_cidr_blocks = [local.allowed_app_cidr] 82 | } 83 | 84 | # 85 | # Create a security group for port 443 traffic 86 | # 87 | module "web_server_secure_sg" { 88 | source = "terraform-aws-modules/security-group/aws//modules/https-443" 89 | 90 | name = format("%s-web-server-secure-%s", local.prefix, random_id.id.hex) 91 | description = "Security group for web-server with HTTPS ports" 92 | vpc_id = module.vpc.vpc_id 93 | 94 | ingress_cidr_blocks = [local.allowed_app_cidr] 95 | } 96 | 97 | # 98 | # Create a security group for SSH traffic 99 | # 100 | module "ssh_secure_sg" { 101 | source = "terraform-aws-modules/security-group/aws//modules/ssh" 102 | 103 | name = format("%s-ssh-%s", local.prefix, random_id.id.hex) 104 | description = "Security group for SSH ports open within VPC" 105 | vpc_id = module.vpc.vpc_id 106 | 107 | ingress_cidr_blocks = [local.allowed_mgmt_cidr] 108 | } 109 | 110 | # 111 | # Create BIG-IP 112 | # 113 | module bigip { 114 | source = "../../" 115 | 116 | prefix = format( 117 | "%s-bigip-3-nic_with_new_vpc-%s", 118 | local.prefix, 119 | random_id.id.hex 120 | ) 121 | f5_instance_count = length(local.azs) 122 | ec2_instance_type = "m5.large" 123 | ec2_key_name = var.ec2_key_name 124 | aws_secretmanager_secret_id = aws_secretsmanager_secret.bigip.id 125 | mgmt_subnet_security_group_ids = [ 126 | module.web_server_secure_sg.this_security_group_id, 127 | module.ssh_secure_sg.this_security_group_id 128 | ] 129 | 130 | public_subnet_security_group_ids = [ 131 | module.web_server_sg.this_security_group_id, 132 | module.web_server_secure_sg.this_security_group_id 133 | ] 134 | 135 | private_subnet_security_group_ids = [ 136 | module.vpc.default_security_group_id 137 | ] 138 | 139 | vpc_public_subnet_ids = module.vpc.public_subnets 140 | vpc_private_subnet_ids = module.vpc.private_subnets 141 | vpc_mgmt_subnet_ids = module.vpc.database_subnets 142 | } 143 | 144 | # 145 | # Variables used by this example 146 | # 147 | locals { 148 | prefix = "tf-aws-bigip" 149 | region = "us-east-2" 150 | azs = [format("%s%s", local.region, "a"), format("%s%s", local.region, "b")] 151 | cidr = "10.0.0.0/16" 152 | allowed_mgmt_cidr = "0.0.0.0/0" 153 | allowed_app_cidr = "0.0.0.0/0" 154 | } 155 | -------------------------------------------------------------------------------- /examples/3_nic_with_new_vpc/outputs.tf: -------------------------------------------------------------------------------- 1 | # VPC 2 | output "vpc_id" { 3 | value = module.vpc.vpc_id 4 | } 5 | 6 | # BIG-IP Management Public IP Addresses 7 | output "bigip_mgmt_ips" { 8 | value = module.bigip.mgmt_public_ips 9 | } 10 | 11 | # BIG-IP Management Public DNS Address 12 | output "bigip_mgmt_dns" { 13 | value = module.bigip.mgmt_public_dns 14 | } 15 | 16 | # BIG-IP Management Port 17 | output "bigip_mgmt_port" { 18 | value = module.bigip.mgmt_port 19 | } 20 | # BIG-IP Password 21 | output "password" { 22 | value = random_password.password 23 | sensitive = true 24 | } 25 | 26 | # BIG-IP Password Secret name 27 | output "aws_secretmanager_secret_name" { 28 | value = aws_secretsmanager_secret.bigip.name 29 | } 30 | -------------------------------------------------------------------------------- /examples/3_nic_with_new_vpc/variables.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Variable for the EC2 Key 3 | # Set via CLI or via terraform.tfvars file 4 | # 5 | variable "ec2_key_name" { 6 | description = "AWS EC2 Key name for SSH access" 7 | type = string 8 | } 9 | 10 | variable "prefix" { 11 | description = "Prefix for resources created by this module" 12 | type = string 13 | default = "terraform-aws-bigip-3nic" 14 | } 15 | -------------------------------------------------------------------------------- /f5_onboard.tmpl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script must be non-blocking or run in the background. 4 | 5 | mkdir -p /config/cloud 6 | 7 | cat << 'EOF' > /config/cloud/startup-script.sh 8 | 9 | #!/bin/bash 10 | 11 | # BIG-IPS ONBOARD SCRIPT 12 | 13 | LOG_FILE=${onboard_log} 14 | 15 | if [ ! -e $LOG_FILE ] 16 | then 17 | touch $LOG_FILE 18 | exec &>>$LOG_FILE 19 | else 20 | #if file exists, exit as only want to run once 21 | exit 22 | fi 23 | 24 | exec 1>$LOG_FILE 2>&1 25 | 26 | # WAIT FOR BIG-IP SYSTEMS & API TO BE UP 27 | curl -o /config/cloud/utils.sh -s --fail --retry 60 -m 10 -L https://raw.githubusercontent.com/F5Networks/f5-cloud-libs/develop/scripts/util.sh 28 | . /config/cloud/utils.sh 29 | wait_for_bigip 30 | 31 | ### CHECK IF DNS IS CONFIGURED YET, IF NOT, SLEEP 32 | echo "CHECK THAT DNS IS READY" 33 | nslookup github.com 34 | if [ $? -ne 0 ]; then 35 | echo "DNS NOT READY, SLEEP 30 SECS" 36 | sleep 30 37 | fi 38 | 39 | ### GET SECRET VIA DEFAULT REQUESTS LIB ON BIG-IP 40 | echo "GET BIG-IP PASSWORD FROM AWS SECRET MANAGER" 41 | role_name=$(curl -s "http://169.254.169.254/latest/meta-data/iam/security-credentials/" ) 42 | payload=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/$role_name) 43 | export AWS_ACCESS_KEY_ID=$(printf "$payload" | jq -r ".AccessKeyId") 44 | export AWS_SECRET_ACCESS_KEY=$(printf "$payload" | jq -r ".SecretAccessKey") 45 | export AWS_TOKEN=$(printf "$payload" | jq -r ".Token") 46 | export AWS_REGION=`curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone| sed s/.$//` 47 | export SECRET_ID=${secret_id} 48 | 49 | ### WRITE PYTHON FILE TO DISK 50 | cat > secrets_manager.py << END_TEXT 51 | # Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 52 | # 53 | # This file is licensed under the Apache License, Version 2.0 (the "License"). 54 | # You may not use this file except in compliance with the License. A copy of the 55 | # License is located at 56 | # 57 | # http://aws.amazon.com/apache2.0/ 58 | # 59 | # This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 60 | # OF ANY KIND, either express or implied. See the License for the specific 61 | # language governing permissions and limitations under the License. 62 | 63 | # AWS Version 4 signing example 64 | 65 | # See: http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html 66 | # This version makes a POST request and passes request parameters 67 | # in the body (payload) of the request. Auth information is passed in 68 | # an Authorization header. 69 | import sys, os, base64, datetime, hashlib, hmac 70 | import requests 71 | import json 72 | 73 | # Read AWS access key from env. variables or configuration file. Best practice is NOT 74 | # to embed credentials in code. 75 | access_key = os.environ.get('AWS_ACCESS_KEY_ID') 76 | secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY') 77 | token = os.environ.get('AWS_TOKEN') 78 | region = os.environ.get('AWS_REGION') 79 | secret_id = os.environ.get('SECRET_ID') 80 | if access_key is None: 81 | print('No access key was provided.') 82 | sys.exit() 83 | if secret_key is None: 84 | print('No secret key was provided.') 85 | sys.exit() 86 | if token is None: 87 | print('No token was provided.') 88 | sys.exit() 89 | if region is None: 90 | print('No region was provided.') 91 | sys.exit() 92 | if secret_id is None: 93 | print('No secret_id was provided.') 94 | sys.exit() 95 | # ************* REQUEST VALUES ************* 96 | method = 'POST' 97 | service = 'secretsmanager' 98 | host = 'secretsmanager.'+ region + '.amazonaws.com' 99 | endpoint = 'https://secretsmanager.'+ region +'.amazonaws.com/' 100 | content_type = 'application/x-amz-json-1.1' 101 | amz_target = 'secretsmanager.GetSecretValue' 102 | 103 | # Request parameters for GetSecretValue--passed in a JSON block. 104 | request_parameters = '{' 105 | request_parameters += '"SecretId": "'+ secret_id +'"' 106 | request_parameters += '}' 107 | 108 | # Key derivation functions. See: 109 | # http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-python 110 | def sign(key, msg): 111 | return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() 112 | 113 | def getSignatureKey(key, date_stamp, regionName, serviceName): 114 | kDate = sign(('AWS4' + key).encode('utf-8'), date_stamp) 115 | kRegion = sign(kDate, regionName) 116 | kService = sign(kRegion, serviceName) 117 | kSigning = sign(kService, 'aws4_request') 118 | return kSigning 119 | 120 | # Create a date for headers and the credential string 121 | t = datetime.datetime.utcnow() 122 | amz_date = t.strftime('%Y%m%dT%H%M%SZ') 123 | date_stamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope 124 | 125 | 126 | # ************* TASK 1: CREATE A CANONICAL REQUEST ************* 127 | # http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html 128 | 129 | # Step 1 is to define the verb (GET, POST, etc.)--already done. 130 | 131 | # Step 2: Create canonical URI--the part of the URI from domain to query 132 | # string (use '/' if no path) 133 | canonical_uri = '/' 134 | 135 | ## Step 3: Create the canonical query string. In this example, request 136 | # parameters are passed in the body of the request and the query string 137 | # is blank. 138 | canonical_querystring = '' 139 | 140 | # Step 4: Create the canonical headers. Header names must be trimmed 141 | # and lowercase, and sorted in code point order from low to high. 142 | # Note that there is a trailing \n. 143 | canonical_headers = 'content-type:' + content_type + '\n' + 'host:' + host + '\n' + 'x-amz-date:' + amz_date + '\n' + 'x-amz-target:' + amz_target + '\n' 144 | 145 | # Step 5: Create the list of signed headers. This lists the headers 146 | # in the canonical_headers list, delimited with ";" and in alpha order. 147 | # Note: The request can include any headers; canonical_headers and 148 | # signed_headers include those that you want to be included in the 149 | # hash of the request. "Host" and "x-amz-date" are always required. 150 | signed_headers = 'content-type;host;x-amz-date;x-amz-target' 151 | 152 | # Step 6: Create payload hash. In this example, the payload (body of 153 | # the request) contains the request parameters. 154 | payload_hash = hashlib.sha256(request_parameters.encode('utf-8')).hexdigest() 155 | 156 | # Step 7: Combine elements to create canonical request 157 | canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash 158 | 159 | # ************* TASK 2: CREATE THE STRING TO SIGN************* 160 | # Match the algorithm to the hashing algorithm you use, either SHA-1 or 161 | # SHA-256 (recommended) 162 | algorithm = 'AWS4-HMAC-SHA256' 163 | credential_scope = date_stamp + '/' + region + '/' + service + '/' + 'aws4_request' 164 | string_to_sign = algorithm + '\n' + amz_date + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 165 | 166 | # ************* TASK 3: CALCULATE THE SIGNATURE ************* 167 | # Create the signing key using the function defined above. 168 | signing_key = getSignatureKey(secret_key, date_stamp, region, service) 169 | 170 | # Sign the string_to_sign using the signing_key 171 | signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() 172 | 173 | # ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST ************* 174 | # Put the signature information in a header named Authorization. 175 | authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature 176 | headers = {'Content-Type':content_type, 177 | 'X-Amz-Date':amz_date, 178 | 'X-Amz-Target':amz_target, 179 | 'Authorization':authorization_header, 180 | 'X-Amz-Security-Token': token} 181 | 182 | 183 | # ************* SEND THE REQUEST ************* 184 | 185 | r = requests.post(endpoint, request_parameters, headers=headers) 186 | 187 | print(r.text) 188 | END_TEXT 189 | 190 | ### SET BIG-IP PASSWORD 191 | echo "SET THE BIG-IP PASSWORD" 192 | pwd="$(python secrets_manager.py | jq -r '.SecretString')" 193 | if [ -z "$pwd" ] 194 | then 195 | echo "ERROR: UNABLE TO OBTAIN PASSWORD" 196 | else 197 | tmsh modify auth user admin password "$pwd" 198 | fi 199 | 200 | ### DOWNLOAD ONBOARDING PKGS 201 | # Could be pre-packaged or hosted internally 202 | mkdir -p ${libs_dir} 203 | 204 | DO_URL='${DO_URL}' 205 | DO_FN=$(basename "$DO_URL") 206 | AS3_URL='${AS3_URL}' 207 | AS3_FN=$(basename "$AS3_URL") 208 | TS_URL='${TS_URL}' 209 | TS_FN=$(basename "$TS_URL") 210 | 211 | echo -e "\n"$(date) "Download Declarative Onboarding Pkg" 212 | curl -L -o ${libs_dir}/$DO_FN $DO_URL 213 | sleep 20 214 | 215 | echo -e "\n"$(date) "Download AS3 Pkg" 216 | curl -L -o ${libs_dir}/$AS3_FN $AS3_URL 217 | sleep 20 218 | 219 | echo -e "\n"$(date) "Download TS Pkg" 220 | curl -L -o ${libs_dir}/$TS_FN $TS_URL 221 | sleep 20 222 | 223 | # Copy the RPM Pkg to the file location 224 | cp ${libs_dir}/*.rpm /var/config/rest/downloads/ 225 | 226 | # Install Declarative Onboarding Pkg 227 | DATA="{\"operation\":\"INSTALL\",\"packageFilePath\":\"/var/config/rest/downloads/$DO_FN\"}" 228 | echo -e "\n"$(date) "Install DO Pkg" 229 | restcurl -X POST "shared/iapp/package-management-tasks" -d $DATA 230 | 231 | # Install AS3 Pkg 232 | DATA="{\"operation\":\"INSTALL\",\"packageFilePath\":\"/var/config/rest/downloads/$AS3_FN\"}" 233 | echo -e "\n"$(date) "Install AS3 Pkg" 234 | restcurl -X POST "shared/iapp/package-management-tasks" -d $DATA 235 | 236 | # Install TS Pkg 237 | DATA="{\"operation\":\"INSTALL\",\"packageFilePath\":\"/var/config/rest/downloads/$TS_FN\"}" 238 | echo -e "\n"$(date) "Install TS Pkg" 239 | restcurl -X POST "shared/iapp/package-management-tasks" -d $DATA 240 | 241 | 242 | date 243 | echo "FINISHED STARTUP SCRIPT" 244 | 245 | ### Clean up 246 | rm /config/cloud/startup-script.sh 247 | EOF 248 | 249 | # Now run in the background to not block startup 250 | chmod 755 /config/cloud/startup-script.sh 251 | nohup /config/cloud/startup-script.sh & -------------------------------------------------------------------------------- /iam.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Create IAM Role 3 | # 4 | 5 | data "aws_iam_policy_document" "bigip_role" { 6 | version = "2012-10-17" 7 | statement { 8 | actions = [ 9 | "sts:AssumeRole" 10 | ] 11 | principals { 12 | type = "Service" 13 | identifiers = ["ec2.amazonaws.com"] 14 | } 15 | } 16 | } 17 | 18 | resource "aws_iam_role" "bigip_role" { 19 | name = format("%s-bigip-role", var.prefix) 20 | assume_role_policy = data.aws_iam_policy_document.bigip_role.json 21 | 22 | tags = { 23 | tag-key = "tag-value" 24 | } 25 | } 26 | 27 | resource "aws_iam_instance_profile" "bigip_profile" { 28 | name = format("%s-bigip-profile", var.prefix) 29 | role = aws_iam_role.bigip_role.name 30 | } 31 | 32 | data "aws_iam_policy_document" "bigip_policy" { 33 | version = "2012-10-17" 34 | statement { 35 | actions = [ 36 | "secretsmanager:GetSecretValue" 37 | ] 38 | 39 | resources = [ 40 | data.aws_secretsmanager_secret.password.arn 41 | ] 42 | } 43 | } 44 | 45 | resource "aws_iam_role_policy" "bigip_policy" { 46 | name = format("%s-bigip-policy", var.prefix) 47 | role = aws_iam_role.bigip_role.id 48 | policy = data.aws_iam_policy_document.bigip_policy.json 49 | } 50 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Ensure Secret exists 3 | # 4 | data "aws_secretsmanager_secret" "password" { 5 | name = var.aws_secretmanager_secret_id 6 | } 7 | 8 | # 9 | # Find BIG-IP AMI 10 | # 11 | data "aws_ami" "f5_ami" { 12 | most_recent = true 13 | owners = ["679593333241"] 14 | 15 | filter { 16 | name = "name" 17 | values = [var.f5_ami_search_name] 18 | } 19 | } 20 | 21 | # 22 | # Create Management Network Interfaces 23 | # 24 | resource "aws_network_interface" "mgmt" { 25 | count = length(var.vpc_mgmt_subnet_ids) 26 | subnet_id = var.vpc_mgmt_subnet_ids[count.index] 27 | security_groups = var.mgmt_subnet_security_group_ids 28 | } 29 | 30 | # 31 | # add an elastic IP to the BIG-IP management interface 32 | # 33 | resource "aws_eip" "mgmt" { 34 | count = var.mgmt_eip ? length(var.vpc_mgmt_subnet_ids) : 0 35 | network_interface = aws_network_interface.mgmt[count.index].id 36 | vpc = true 37 | } 38 | 39 | # 40 | # Create Public Network Interfaces 41 | # 42 | resource "aws_network_interface" "public" { 43 | count = length(var.vpc_public_subnet_ids) 44 | subnet_id = var.vpc_public_subnet_ids[count.index] 45 | security_groups = var.public_subnet_security_group_ids 46 | private_ips_count = var.application_endpoint_count 47 | } 48 | 49 | # 50 | # Create Private Network Interfaces 51 | # 52 | resource "aws_network_interface" "private" { 53 | count = length(var.vpc_private_subnet_ids) 54 | subnet_id = var.vpc_private_subnet_ids[count.index] 55 | security_groups = var.private_subnet_security_group_ids 56 | } 57 | 58 | # 59 | # Deploy BIG-IP 60 | # 61 | resource "aws_instance" "f5_bigip" { 62 | # determine the number of BIG-IPs to deploy 63 | count = var.f5_instance_count 64 | instance_type = var.ec2_instance_type 65 | ami = data.aws_ami.f5_ami.id 66 | iam_instance_profile = aws_iam_instance_profile.bigip_profile.name 67 | 68 | key_name = var.ec2_key_name 69 | 70 | root_block_device { 71 | delete_on_termination = true 72 | } 73 | 74 | # set the mgmt interface 75 | dynamic "network_interface" { 76 | for_each = toset([aws_network_interface.mgmt[count.index].id]) 77 | 78 | content { 79 | network_interface_id = network_interface.value 80 | device_index = 0 81 | } 82 | } 83 | 84 | # set the public interface only if an interface is defined 85 | dynamic "network_interface" { 86 | for_each = length(aws_network_interface.public) > count.index ? toset([aws_network_interface.public[count.index].id]) : toset([]) 87 | 88 | content { 89 | network_interface_id = network_interface.value 90 | device_index = 1 91 | } 92 | } 93 | 94 | 95 | # set the private interface only if an interface is defined 96 | dynamic "network_interface" { 97 | for_each = length(aws_network_interface.private) > count.index ? toset([aws_network_interface.private[count.index].id]) : toset([]) 98 | 99 | content { 100 | network_interface_id = network_interface.value 101 | device_index = 2 102 | } 103 | } 104 | 105 | # build user_data file from template 106 | user_data = templatefile( 107 | "${path.module}/f5_onboard.tmpl", 108 | { 109 | DO_URL = var.DO_URL, 110 | AS3_URL = var.AS3_URL, 111 | TS_URL = var.TS_URL, 112 | libs_dir = var.libs_dir, 113 | onboard_log = var.onboard_log, 114 | secret_id = var.aws_secretmanager_secret_id 115 | } 116 | ) 117 | 118 | depends_on = [aws_eip.mgmt] 119 | 120 | tags = { 121 | Name = format("%s-%d", var.prefix, count.index) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | # BIG-IP Management Public IP Addresses 2 | output "deprecation_notice" { 3 | value = "This module is no longer under active development. Please refere to the module README for more information." 4 | } 5 | 6 | output "mgmt_public_ips" { 7 | description = "List of BIG-IP public IP addresses for the management interfaces" 8 | value = aws_eip.mgmt[*].public_ip 9 | } 10 | 11 | # BIG-IP Management Public DNS 12 | output "mgmt_public_dns" { 13 | description = "List of BIG-IP public DNS records for the management interfaces" 14 | value = aws_eip.mgmt[*].public_dns 15 | } 16 | 17 | # BIG-IP Management Port 18 | output "mgmt_port" { 19 | description = "HTTPS Port used for the BIG-IP management interface" 20 | value = length(var.vpc_public_subnet_ids) > 0 ? "443" : "8443" 21 | } 22 | 23 | # Public Network Interface 24 | output "public_nic_ids" { 25 | description = "List of BIG-IP public network interface ids" 26 | value = aws_network_interface.public[*].id 27 | } 28 | 29 | output "mgmt_addresses" { 30 | description = "List of BIG-IP management addresses" 31 | value = aws_network_interface.mgmt[*].private_ips 32 | } 33 | 34 | output "public_addresses" { 35 | description = "List of BIG-IP public addresses" 36 | value = aws_network_interface.public[*].private_ips 37 | } 38 | 39 | output "private_addresses" { 40 | description = "List of BIG-IP private addresses" 41 | value = aws_network_interface.private[*].private_ips 42 | } -------------------------------------------------------------------------------- /tests/1_nic_with_new_vpc_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "encoding/base64" 7 | "fmt" 8 | "testing" 9 | "net/http" 10 | "crypto/tls" 11 | "time" 12 | "os" 13 | "github.com/hashicorp/go-retryablehttp" 14 | "github.com/gruntwork-io/terratest/modules/terraform" 15 | "github.com/aws/aws-sdk-go/service/secretsmanager" 16 | "github.com/aws/aws-sdk-go/aws" 17 | "github.com/aws/aws-sdk-go/aws/awserr" 18 | "github.com/aws/aws-sdk-go/aws/session" 19 | ) 20 | 21 | // AWS Code snippit to obtain secret 22 | func getSecret(secretName string, region string) (password string, err error) { 23 | // Create a session 24 | sess:= session.Must(session.NewSession(&aws.Config{ 25 | Region : aws.String(region), 26 | })) 27 | 28 | //Create a Secrets Manager client 29 | svc := secretsmanager.New(sess) 30 | input := &secretsmanager.GetSecretValueInput{ 31 | SecretId: aws.String(secretName), 32 | VersionStage: aws.String("AWSCURRENT"), // VersionStage defaults to AWSCURRENT if unspecified 33 | } 34 | 35 | // In this sample we only handle the specific exceptions for the 'GetSecretValue' API. 36 | // See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html 37 | 38 | result, err := svc.GetSecretValue(input) 39 | if err != nil { 40 | if aerr, ok := err.(awserr.Error); ok { 41 | switch aerr.Code() { 42 | case secretsmanager.ErrCodeDecryptionFailure: 43 | // Secrets Manager can't decrypt the protected secret text using the provided KMS key. 44 | fmt.Println(secretsmanager.ErrCodeDecryptionFailure, aerr.Error()) 45 | 46 | case secretsmanager.ErrCodeInternalServiceError: 47 | // An error occurred on the server side. 48 | fmt.Println(secretsmanager.ErrCodeInternalServiceError, aerr.Error()) 49 | 50 | case secretsmanager.ErrCodeInvalidParameterException: 51 | // You provided an invalid value for a parameter. 52 | fmt.Println(secretsmanager.ErrCodeInvalidParameterException, aerr.Error()) 53 | 54 | case secretsmanager.ErrCodeInvalidRequestException: 55 | // You provided a parameter value that is not valid for the current state of the resource. 56 | fmt.Println(secretsmanager.ErrCodeInvalidRequestException, aerr.Error()) 57 | 58 | case secretsmanager.ErrCodeResourceNotFoundException: 59 | // We can't find the resource that you asked for. 60 | fmt.Println(secretsmanager.ErrCodeResourceNotFoundException, aerr.Error()) 61 | } 62 | } else { 63 | // Print the error, cast err to awserr.Error to get the Code and 64 | // Message from an error. 65 | fmt.Println(err.Error()) 66 | } 67 | return "", err 68 | } 69 | 70 | // Decrypts secret using the associated KMS CMK. 71 | // Depending on whether the secret is a string or binary, one of these fields will be populated. 72 | var secretString, decodedBinarySecret string 73 | if result.SecretString != nil { 74 | secretString = *result.SecretString 75 | return secretString, nil 76 | } else { 77 | decodedBinarySecretBytes := make([]byte, base64.StdEncoding.DecodedLen(len(result.SecretBinary))) 78 | len, err := base64.StdEncoding.Decode(decodedBinarySecretBytes, result.SecretBinary) 79 | if err != nil { 80 | fmt.Println("Base64 Decode Error:", err) 81 | return "", err 82 | } 83 | decodedBinarySecret = string(decodedBinarySecretBytes[:len]) 84 | return decodedBinarySecret, nil 85 | } 86 | } 87 | 88 | func testAnOToolchain(url string, pwd string, client *retryablehttp.Client) (int, error) { 89 | req, err := retryablehttp.NewRequest("GET", url, nil) 90 | req.SetBasicAuth("admin", pwd) 91 | resp, err := client.Do(req) 92 | if err != nil || resp.StatusCode != 200 { 93 | return 0, errors.New("Request Failed") 94 | } 95 | defer resp.Body.Close() 96 | return resp.StatusCode, nil 97 | } 98 | 99 | // Test the BIG-IP 1 NIC example 100 | // Ensure your AWS credentials are set to the following environment vars: 101 | // AWS_ACCESS_KEY_ID 102 | // AWS_SECRET_ACCESS_KEY 103 | // Ensure you have defined the default AWS region in the following environment var: 104 | // AWS_DEFAULT_REGION 105 | func Test1NicExample(t *testing.T) { 106 | opts := &terraform.Options{ 107 | TerraformDir: "../examples/1_nic_with_new_vpc", 108 | Vars: map[string]interface{} { 109 | "ec2_key_name": os.Getenv("ec2_key_name"), 110 | }, 111 | } 112 | 113 | // Clean up everything at the end of the test 114 | defer terraform.Destroy(t, opts) 115 | 116 | // Deploy BIG-IP 117 | terraform.InitAndApply(t, opts) 118 | 119 | // Get the BIG-IP management IP address 120 | bigipMgmtDNS := terraform.OutputList(t, opts, "bigip_mgmt_dns") 121 | bigipMgmtPort := terraform.OutputRequired(t, opts, "bigip_mgmt_port") 122 | awsSecretmanagerSecretName := terraform.OutputRequired(t, opts, "aws_secretmanager_secret_name") 123 | bigipPwd, err := getSecret(awsSecretmanagerSecretName, os.Getenv("AWS_DEFAULT_REGION")) 124 | 125 | if err != nil || bigipPwd == "" { 126 | t.Errorf("CAN NOT OBTAIN BIG-IP PASSWORD FROM SECRET MANAGER") 127 | } 128 | 129 | // Sleep for 5 minutes (time to boot BIG-IP) so we do not overwhelm restnoded while installing A&O Toolchain 130 | fmt.Println("Sleeping for 5 minutes so A&O Toolchain can be installed") 131 | time.Sleep(300 * time.Second) 132 | 133 | const minRetryTime = 1 // seconds 134 | const maxRetryTime = 120 // seconds 135 | const maxRetryCount = 5 136 | const attemptTimeoutInit = 2 // seconds 137 | const doInfoURL = "/mgmt/shared/declarative-onboarding/info" 138 | const as3InfoURL = "/mgmt/shared/appsvcs/info" 139 | 140 | // since the BIG-IP is deployed with a self-signed cert, we need to ignore validation 141 | tr := &http.Transport{ 142 | TLSClientConfig: &tls.Config { 143 | InsecureSkipVerify: true, 144 | }, 145 | } 146 | 147 | // build an http client with our custom transport 148 | client := &http.Client{ 149 | Transport: tr, 150 | } 151 | 152 | // configure the Hashcorp retryablehttp client 153 | rclient := &retryablehttp.Client{ 154 | HTTPClient: client, 155 | RetryWaitMin: minRetryTime * time.Second, 156 | RetryWaitMax: maxRetryTime * time.Second, 157 | RetryMax: maxRetryCount, 158 | Backoff: retryablehttp.DefaultBackoff, 159 | } 160 | 161 | // AnO Toolchain returns a 400 if the rpm is not installed, retry if a 400 is returned 162 | rclient.CheckRetry = func (_ context.Context, resp *http.Response, err error) (bool, error) { 163 | if err != nil { 164 | return true, err 165 | } 166 | if resp.StatusCode == 0 || resp.StatusCode >= 400 { 167 | fmt.Println(resp.StatusCode) 168 | return true, nil 169 | } 170 | return false, nil 171 | } 172 | 173 | // Check the A&O Toolchain for each BIG-IP 174 | // TODO: bigips is still not in the right format 175 | for _, bigip := range bigipMgmtDNS { 176 | // Check DO info page 177 | fmt.Printf("CHECK DO FOR %s\n", bigip) 178 | DOurl := fmt.Sprintf("https://%s:%s%s", bigip, bigipMgmtPort, doInfoURL) 179 | doresp, err := testAnOToolchain(DOurl, bigipPwd, rclient) 180 | if err != nil { 181 | t.Errorf("DO REQUEST FAILED") 182 | } 183 | fmt.Println(doresp) 184 | 185 | // Check AS3 info page 186 | fmt.Printf("CHECK AS3 FOR %s\n", bigip) 187 | AS3url := fmt.Sprintf("https://%s:%s%s", bigip, bigipMgmtPort, as3InfoURL) 188 | as3resp, err := testAnOToolchain(AS3url, bigipPwd, rclient) 189 | if err != nil { 190 | t.Errorf("DO REQUEST FAILED") 191 | } 192 | fmt.Println(as3resp) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /tests/3_nic_with_new_vpc_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "encoding/base64" 7 | "fmt" 8 | "testing" 9 | "net/http" 10 | "crypto/tls" 11 | "time" 12 | "os" 13 | "github.com/hashicorp/go-retryablehttp" 14 | "github.com/gruntwork-io/terratest/modules/terraform" 15 | "github.com/aws/aws-sdk-go/service/secretsmanager" 16 | "github.com/aws/aws-sdk-go/aws" 17 | "github.com/aws/aws-sdk-go/aws/awserr" 18 | "github.com/aws/aws-sdk-go/aws/session" 19 | ) 20 | 21 | // AWS Code snippit to obtain secret 22 | func getSecret3(secretName string, region string) (password string, err error) { 23 | // Create a session 24 | sess:= session.Must(session.NewSession(&aws.Config{ 25 | Region : aws.String(region), 26 | })) 27 | 28 | //Create a Secrets Manager client 29 | svc := secretsmanager.New(sess) 30 | input := &secretsmanager.GetSecretValueInput{ 31 | SecretId: aws.String(secretName), 32 | VersionStage: aws.String("AWSCURRENT"), // VersionStage defaults to AWSCURRENT if unspecified 33 | } 34 | 35 | // In this sample we only handle the specific exceptions for the 'GetSecretValue' API. 36 | // See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html 37 | 38 | result, err := svc.GetSecretValue(input) 39 | if err != nil { 40 | if aerr, ok := err.(awserr.Error); ok { 41 | switch aerr.Code() { 42 | case secretsmanager.ErrCodeDecryptionFailure: 43 | // Secrets Manager can't decrypt the protected secret text using the provided KMS key. 44 | fmt.Println(secretsmanager.ErrCodeDecryptionFailure, aerr.Error()) 45 | 46 | case secretsmanager.ErrCodeInternalServiceError: 47 | // An error occurred on the server side. 48 | fmt.Println(secretsmanager.ErrCodeInternalServiceError, aerr.Error()) 49 | 50 | case secretsmanager.ErrCodeInvalidParameterException: 51 | // You provided an invalid value for a parameter. 52 | fmt.Println(secretsmanager.ErrCodeInvalidParameterException, aerr.Error()) 53 | 54 | case secretsmanager.ErrCodeInvalidRequestException: 55 | // You provided a parameter value that is not valid for the current state of the resource. 56 | fmt.Println(secretsmanager.ErrCodeInvalidRequestException, aerr.Error()) 57 | 58 | case secretsmanager.ErrCodeResourceNotFoundException: 59 | // We can't find the resource that you asked for. 60 | fmt.Println(secretsmanager.ErrCodeResourceNotFoundException, aerr.Error()) 61 | } 62 | } else { 63 | // Print the error, cast err to awserr.Error to get the Code and 64 | // Message from an error. 65 | fmt.Println(err.Error()) 66 | } 67 | return "", err 68 | } 69 | 70 | // Decrypts secret using the associated KMS CMK. 71 | // Depending on whether the secret is a string or binary, one of these fields will be populated. 72 | var secretString, decodedBinarySecret string 73 | if result.SecretString != nil { 74 | secretString = *result.SecretString 75 | return secretString, nil 76 | } else { 77 | decodedBinarySecretBytes := make([]byte, base64.StdEncoding.DecodedLen(len(result.SecretBinary))) 78 | len, err := base64.StdEncoding.Decode(decodedBinarySecretBytes, result.SecretBinary) 79 | if err != nil { 80 | fmt.Println("Base64 Decode Error:", err) 81 | return "", err 82 | } 83 | decodedBinarySecret = string(decodedBinarySecretBytes[:len]) 84 | return decodedBinarySecret, nil 85 | } 86 | } 87 | 88 | func testAnOToolchain3(url string, pwd string, client *retryablehttp.Client) (int, error) { 89 | req, err := retryablehttp.NewRequest("GET", url, nil) 90 | req.SetBasicAuth("admin", pwd) 91 | resp, err := client.Do(req) 92 | if err != nil || resp.StatusCode != 200 { 93 | return 0, errors.New("Request Failed") 94 | } 95 | defer resp.Body.Close() 96 | return resp.StatusCode, nil 97 | } 98 | 99 | // Test the BIG-IP 1 NIC example 100 | // Ensure your AWS credentials are set to the following environment vars: 101 | // AWS_ACCESS_KEY_ID 102 | // AWS_SECRET_ACCESS_KEY 103 | // Ensure you have defined the default AWS region in the following environment var: 104 | // AWS_DEFAULT_REGION 105 | func Test3NicExample(t *testing.T) { 106 | opts := &terraform.Options{ 107 | TerraformDir: "../examples/3_nic_with_new_vpc", 108 | Vars: map[string]interface{} { 109 | "ec2_key_name": os.Getenv("ec2_key_name"), 110 | }, 111 | } 112 | 113 | // Clean up everything at the end of the test 114 | defer terraform.Destroy(t, opts) 115 | 116 | // Deploy BIG-IP 117 | terraform.InitAndApply(t, opts) 118 | 119 | // Get the BIG-IP management IP address 120 | bigipMgmtDNS := terraform.OutputList(t, opts, "bigip_mgmt_dns") 121 | bigipMgmtPort := terraform.OutputRequired(t, opts, "bigip_mgmt_port") 122 | awsSecretmanagerSecretName := terraform.OutputRequired(t, opts, "aws_secretmanager_secret_name") 123 | bigipPwd, err := getSecret3(awsSecretmanagerSecretName, os.Getenv("AWS_DEFAULT_REGION")) 124 | 125 | if err != nil || bigipPwd == "" { 126 | t.Errorf("CAN NOT OBTAIN BIG-IP PASSWORD FROM SECRET MANAGER") 127 | } 128 | 129 | // Sleep for 5 minutes (time to boot BIG-IP) so we do not overwhelm restnoded while installing A&O Toolchain 130 | fmt.Println("Sleeping for 5 minutes so A&O Toolchain can be installed") 131 | time.Sleep(300 * time.Second) 132 | 133 | const minRetryTime = 1 // seconds 134 | const maxRetryTime = 120 // seconds 135 | const maxRetryCount = 5 136 | const attemptTimeoutInit = 2 // seconds 137 | const doInfoURL = "/mgmt/shared/declarative-onboarding/info" 138 | const as3InfoURL = "/mgmt/shared/appsvcs/info" 139 | 140 | // since the BIG-IP is deployed with a self-signed cert, we need to ignore validation 141 | tr := &http.Transport{ 142 | TLSClientConfig: &tls.Config { 143 | InsecureSkipVerify: true, 144 | }, 145 | } 146 | 147 | // build an http client with our custom transport 148 | client := &http.Client{ 149 | Transport: tr, 150 | } 151 | 152 | // configure the Hashcorp retryablehttp client 153 | rclient := &retryablehttp.Client{ 154 | HTTPClient: client, 155 | RetryWaitMin: minRetryTime * time.Second, 156 | RetryWaitMax: maxRetryTime * time.Second, 157 | RetryMax: maxRetryCount, 158 | Backoff: retryablehttp.DefaultBackoff, 159 | } 160 | 161 | // AnO Toolchain returns a 400 if the rpm is not installed, retry if a 400 is returned 162 | rclient.CheckRetry = func (_ context.Context, resp *http.Response, err error) (bool, error) { 163 | if err != nil { 164 | return true, err 165 | } 166 | if resp.StatusCode == 0 || resp.StatusCode >= 400 { 167 | fmt.Println(resp.StatusCode) 168 | return true, nil 169 | } 170 | return false, nil 171 | } 172 | 173 | // Check the A&O Toolchain for each BIG-IP 174 | // TODO: bigips is still not in the right format 175 | for _, bigip := range bigipMgmtDNS { 176 | // Check DO info page 177 | fmt.Printf("CHECK DO FOR %s\n", bigip) 178 | DOurl := fmt.Sprintf("https://%s:%s%s", bigip, bigipMgmtPort, doInfoURL) 179 | doresp, err := testAnOToolchain3(DOurl, bigipPwd, rclient) 180 | if err != nil { 181 | t.Errorf("DO REQUEST FAILED") 182 | } 183 | fmt.Println(doresp) 184 | 185 | // Check AS3 info page 186 | fmt.Printf("CHECK AS3 FOR %s\n", bigip) 187 | AS3url := fmt.Sprintf("https://%s:%s%s", bigip, bigipMgmtPort, as3InfoURL) 188 | as3resp, err := testAnOToolchain3(AS3url, bigipPwd, rclient) 189 | if err != nil { 190 | t.Errorf("DO REQUEST FAILED") 191 | } 192 | fmt.Println(as3resp) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all dep test 2 | 3 | all: dep test 4 | 5 | dep: 6 | dep init 7 | 8 | test: 9 | go test -v -count=1 -timeout 30m -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "prefix" { 2 | description = "Prefix for resources created by this module" 3 | type = string 4 | default = "terraform-aws-bigip-demo" 5 | } 6 | 7 | variable "f5_ami_search_name" { 8 | description = "BIG-IP AMI name to search for" 9 | type = string 10 | default = "F5 Networks BIGIP-14.* PAYG - Best 200Mbps*" 11 | } 12 | 13 | variable "f5_instance_count" { 14 | description = "Number of BIG-IPs to deploy" 15 | type = number 16 | default = 1 17 | } 18 | 19 | variable "application_endpoint_count" { 20 | description = "number of public application addresses to assign" 21 | type = number 22 | default = 2 23 | } 24 | 25 | variable "ec2_instance_type" { 26 | description = "AWS EC2 instance type" 27 | type = string 28 | default = "m4.large" 29 | } 30 | 31 | variable "ec2_key_name" { 32 | description = "AWS EC2 Key name for SSH access" 33 | type = string 34 | } 35 | 36 | variable "vpc_public_subnet_ids" { 37 | description = "AWS VPC Subnet id for the public subnet" 38 | type = list 39 | default = [] 40 | } 41 | 42 | variable "vpc_private_subnet_ids" { 43 | description = "AWS VPC Subnet id for the private subnet" 44 | type = list 45 | default = [] 46 | } 47 | 48 | variable "vpc_mgmt_subnet_ids" { 49 | description = "AWS VPC Subnet id for the management subnet" 50 | type = list 51 | default = [] 52 | } 53 | 54 | variable "mgmt_eip" { 55 | description = "Enable an Elastic IP address on the management interface" 56 | type = bool 57 | default = true 58 | } 59 | 60 | variable "mgmt_subnet_security_group_ids" { 61 | description = "AWS Security Group ID for BIG-IP management interface" 62 | type = list 63 | default = [] 64 | } 65 | 66 | variable "public_subnet_security_group_ids" { 67 | description = "AWS Security Group ID for BIG-IP public interface" 68 | type = list 69 | default = [] 70 | } 71 | 72 | variable "private_subnet_security_group_ids" { 73 | description = "AWS Security Group ID for BIG-IP private interface" 74 | type = list 75 | default = [] 76 | } 77 | 78 | variable "aws_secretmanager_secret_id" { 79 | description = "AWS Secret Manager Secret ID that stores the BIG-IP password" 80 | type = string 81 | } 82 | 83 | 84 | ## Please check and update the latest DO URL from https://github.com/F5Networks/f5-declarative-onboarding/releases 85 | # always point to a specific version in order to avoid inadvertent configuration inconsistency 86 | variable DO_URL { 87 | description = "URL to download the BIG-IP Declarative Onboarding module" 88 | type = string 89 | default = "https://github.com/F5Networks/f5-declarative-onboarding/releases/download/v1.8.0/f5-declarative-onboarding-1.8.0-2.noarch.rpm" 90 | } 91 | ## Please check and update the latest AS3 URL from https://github.com/F5Networks/f5-appsvcs-extension/releases/latest 92 | # always point to a specific version in order to avoid inadvertent configuration inconsistency 93 | variable AS3_URL { 94 | description = "URL to download the BIG-IP Application Service Extension 3 (AS3) module" 95 | type = string 96 | default = "https://github.com/F5Networks/f5-appsvcs-extension/releases/download/v3.14.0/f5-appsvcs-3.14.0-4.noarch.rpm" 97 | } 98 | 99 | ## Please check and update the latest TS URL from https://github.com/F5Networks/f5-telemetry-streaming/releases/latest 100 | # always point to a specific version in order to avoid inadvertent configuration inconsistency 101 | variable TS_URL { 102 | description = "URL to download the BIG-IP Telemetry Streaming Extension (TS) module" 103 | type = string 104 | default = "https://github.com/F5Networks/f5-telemetry-streaming/releases/download/v1.8.0/f5-telemetry-1.8.0-1.noarch.rpm" 105 | } 106 | 107 | variable "libs_dir" { 108 | description = "Directory on the BIG-IP to download the A&O Toolchain into" 109 | type = string 110 | default = "/config/cloud/aws/node_modules" 111 | } 112 | 113 | variable onboard_log { 114 | description = "Directory on the BIG-IP to store the cloud-init logs" 115 | type = string 116 | default = "/var/log/startup-script.log" 117 | } 118 | -------------------------------------------------------------------------------- /versions: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.12" 3 | } --------------------------------------------------------------------------------