Skip to content

Commit 2d5333b

Browse files
committed
feat: 🎸 Initial commit
Inintial population of repository
1 parent 6484ca4 commit 2d5333b

File tree

17 files changed

+472
-2
lines changed

17 files changed

+472
-2
lines changed

README.md

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,104 @@
1-
# .github
2-
This repo is used for default community health files, such as CONTRIBUTING and CODE_OF_CONDUCT, for this organization. Default files will be used for any public repository in this organization that does not contain its own of that type.
1+
# terraform-aws-ip-address-release
2+
3+
Sometimes AWS fails to release an allocated IP address when tearing down the associated resources. This lambda will release/delete all network interfaces that are in `Status: Available` as they are not associated with a current AWS resource but can't be used by a new AWS resource.
4+
5+
An exception is made for ENIs attached to DataSync tasks since DataSync only establishes ENIs at task creation time.
6+
7+
This includes a 24 hour cloudwatch alarm to trigger the lambda regularly in an effort to keep the account clean and make the resources available for another consumer.
8+
9+
<!-- BEGIN_TF_DOCS -->
10+
## Requirements
11+
12+
| Name | Version |
13+
| ------------------------------------------------------------------------- | -------- |
14+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 0.14 |
15+
| <a name="requirement_archive"></a> [archive](#requirement\_archive) | ~> 2.2 |
16+
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | > 4.0 |
17+
| <a name="requirement_random"></a> [random](#requirement\_random) | >= 3.1.0 |
18+
19+
## Providers
20+
21+
| Name | Version |
22+
| ------------------------------------------------------------- | ------- |
23+
| <a name="provider_archive"></a> [archive](#provider\_archive) | ~> 2.2 |
24+
| <a name="provider_aws"></a> [aws](#provider\_aws) | > 4.0 |
25+
26+
## Modules
27+
28+
| Name | Source | Version |
29+
| --------------------------------------------- | ------ | ------- |
30+
| <a name="module_iam"></a> [iam](#module\_iam) | ./iam | n/a |
31+
32+
## Resources
33+
34+
| Name | Type |
35+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
36+
| [aws_cloudwatch_event_rule.ip_address_release_lambda_interval](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource |
37+
| [aws_cloudwatch_event_target.ip_address_release_lambda_attach](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource |
38+
| [aws_lambda_function.ip_address_release_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource |
39+
| [aws_lambda_permission.event_permission](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource |
40+
| [archive_file.lambda_source](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source |
41+
| [aws_security_group.https-internet-egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/security_group) | data source |
42+
| [aws_vpc.internal](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) | data source |
43+
44+
## Inputs
45+
46+
| Name | Description | Type | Default | Required |
47+
| ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------- | ------- | :------: |
48+
| <a name="input_account_name"></a> [account\_name](#input\_account\_name) | The account name for use in alarm description. | `string` | n/a | yes |
49+
| <a name="input_create_iam_role"></a> [create\_iam\_role](#input\_create\_iam\_role) | Pass in `false` if you are supplying an IAM role. | `bool` | `true` | no |
50+
| <a name="input_iam_role_arn"></a> [iam\_role\_arn](#input\_iam\_role\_arn) | The ARN of the IAM Role to use (creates a new one if set to `null`) | `string` | `null` | no |
51+
| <a name="input_internet_egress_security_group"></a> [internet\_egress\_security\_group](#input\_internet\_egress\_security\_group) | Name of a security group that allows internet outbound calls to port 443 | `string` | n/a | yes |
52+
| <a name="input_permissions_boundary_arn"></a> [permissions\_boundary\_arn](#input\_permissions\_boundary\_arn) | The ARN of the policy that is used to set the permissions boundary for the IAM roles. | `string` | `null` | no |
53+
| <a name="input_subnet_ids"></a> [subnet\_ids](#input\_subnet\_ids) | Subnets that Lambda will be created with in the VPC | `list(string)` | `[]` | no |
54+
| <a name="input_timeout"></a> [timeout](#input\_timeout) | Timeout value for the lambda | `number` | `300` | no |
55+
| <a name="input_usecase"></a> [usecase](#input\_usecase) | Usecase name, can be a team or product name. E.g., 'SRE' | `string` | n/a | yes |
56+
| <a name="input_vpc_id"></a> [vpc\_id](#input\_vpc\_id) | VPC ID to attach the IP Address Release lambda to. Only necessary if there are multiple VPCs in an account. | `string` | `null` | no |
57+
58+
## Outputs
59+
60+
| Name | Description |
61+
| ---------------------------------------------------------------------------- | ------------------------------------------- |
62+
| <a name="output_iam_role_arn"></a> [iam\_role\_arn](#output\_iam\_role\_arn) | The IAM Role created, or the one passed in. |
63+
<!-- END_TF_DOCS -->
64+
65+
# Multi-region deployment
66+
The IAM role created for the initial region can be reused for the second region by referencing the outputs from the first region.
67+
```terraform
68+
* assumes a non-aliased provider is setup elsewhere
69+
module "ip-address-release-primary" {
70+
source = "git::https://github.com/StateFarmIns/terraform-aws-ip-address-release?ref=1.0.0"
71+
72+
providers = {
73+
aws = aws
74+
}
75+
76+
usecase = "SRE"
77+
account_name = var.account_name
78+
permissions_boundary_arn = local.permissions_boundary
79+
internet_egress_security_group_id = data.aws_security_group.https-internet-egress.id
80+
vpc_id = data.aws_vpc.internal.id
81+
}
82+
83+
* assumes an aliased (secondary) provider is setup elsewhere
84+
module "ip-address-release-secondary" {
85+
source = "git::https://github.com/StateFarmIns/terraform-aws-ip-address-release?ref=1.0.0"
86+
87+
providers = {
88+
aws = aws.secondary
89+
}
90+
91+
usecase = "SRE"
92+
account_name = var.account_name
93+
permissions_boundary_arn = local.permissions_boundary
94+
internet_egress_security_group_id = data.aws_security_group.https-internet-egress_secondary.id
95+
iam_role_arn = module.ip-address-release-primary.iam_role_arn # reference the IAM Role created earlier
96+
vpc_id = data.aws_vpc.internal_secondary.id
97+
}
98+
```
99+
100+
# Links
101+
* [Why can't I detach or delete an elastic network interface that Lambda created?](https://aws.amazon.com/premiumsupport/knowledge-center/lambda-eni-find-delete/)
102+
* [Requester Managed Network Interfaces](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/requester-managed-eni.html)
103+
* findassociations script copied from [AWS-support-tools](https://github.com/awslabs/aws-support-tools)
104+

cloudwatch.tf

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This file contains the Cloudwatch alarms that attach to the timer service alarm lambda.
2+
resource "aws_cloudwatch_event_rule" "ip_address_release_lambda_interval" {
3+
name = "${var.usecase}-ip-address-lambda-release-rule"
4+
description = "Fires every 24 hours"
5+
schedule_expression = "rate(24 hours)"
6+
}
7+
8+
resource "aws_cloudwatch_event_target" "ip_address_release_lambda_attach" {
9+
rule = aws_cloudwatch_event_rule.ip_address_release_lambda_interval.name
10+
arn = aws_lambda_function.ip_address_release_lambda.arn
11+
}
12+
13+
resource "aws_lambda_permission" "event_permission" {
14+
statement_id = "AllowExecutionFromCloudWatch"
15+
action = "lambda:InvokeFunction"
16+
function_name = aws_lambda_function.ip_address_release_lambda.function_name
17+
principal = "events.amazonaws.com"
18+
source_arn = aws_cloudwatch_event_rule.ip_address_release_lambda_interval.arn
19+
}

data.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
data "archive_file" "lambda_source" {
2+
type = "zip"
3+
source_dir = "${path.module}/lambda_source"
4+
output_path = "${path.module}/ip_address.zip"
5+
}

iam/data.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
data "aws_caller_identity" "current" {}
2+
data "aws_iam_account_alias" "current" {}
3+
data "aws_region" "current" {}
4+
5+
data "aws_kms_key" "master" {
6+
key_id = "alias/${data.aws_iam_account_alias.current.account_alias}-${data.aws_region.current.name}-master-kmskey"
7+
}

iam/iam.tf

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
resource "random_string" "random" {
2+
special = false
3+
length = 5
4+
}
5+
resource "aws_iam_role" "lambda_role" {
6+
name = "${var.usecase}-ip-address-release-lambda-role-${random_string.random.result}"
7+
assume_role_policy = data.aws_iam_policy_document.lambda_role_trust.json
8+
description = "service role for ip address release lambda"
9+
permissions_boundary = var.permissions_boundary_arn
10+
tags = {
11+
Name = "${var.usecase} lambda role"
12+
}
13+
}
14+
15+
data "aws_iam_policy_document" "lambda_role_trust" {
16+
statement {
17+
effect = "Allow"
18+
actions = ["sts:AssumeRole"]
19+
20+
principals {
21+
type = "Service"
22+
identifiers = ["lambda.amazonaws.com"]
23+
}
24+
}
25+
}
26+
27+
resource "aws_iam_role_policy_attachment" "lambda-policy-attachment" {
28+
role = aws_iam_role.lambda_role.name
29+
policy_arn = aws_iam_policy.lambda_policy.arn
30+
}
31+
32+
resource "aws_iam_policy" "lambda_policy" {
33+
name = "${var.usecase}-ip-address-release-lambda-policy-${random_string.random.result}"
34+
description = "lambda policy for ip address release lambda"
35+
policy = data.aws_iam_policy_document.lambda_policy_document.json
36+
tags = {
37+
Name = "${var.usecase} IP Address Release Lambda Policy"
38+
}
39+
}
40+
41+
data "aws_iam_policy_document" "lambda_policy_document" {
42+
statement {
43+
sid = "lambda"
44+
effect = "Allow"
45+
actions = [
46+
"logs:*",
47+
]
48+
resources = ["*"]
49+
}
50+
statement {
51+
sid = "kms"
52+
effect = "Allow"
53+
actions = [
54+
"kms:ListAliases*",
55+
"kms:CreateGrant",
56+
"kms:Encrypt",
57+
"kms:Decrypt"
58+
]
59+
resources = [
60+
data.aws_kms_key.master.arn
61+
]
62+
}
63+
statement {
64+
sid = "VPC"
65+
effect = "Allow"
66+
actions = [
67+
"ec2:CreateNetworkInterface",
68+
"ec2:DescribeNetworkInterfaces",
69+
"ec2:DeleteNetworkInterface",
70+
"ec2:DescribeSubnets"
71+
]
72+
resources = ["*"]
73+
}
74+
}

iam/outputs.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
output "role_arn" {
2+
value = aws_iam_role.lambda_role.arn
3+
}

iam/variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
variable "usecase" {}
2+
variable "account_name" {}
3+
variable "permissions_boundary_arn" {
4+
type = string
5+
default = null
6+
}

lambda.tf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
resource "aws_lambda_function" "ip_address_release_lambda" {
2+
filename = data.archive_file.lambda_source.output_path
3+
function_name = "${var.usecase}-ip-address-release-lambda"
4+
role = var.iam_role_arn == null ? module.iam[0].role_arn : var.iam_role_arn
5+
handler = "lambda_function.lambda_handler"
6+
source_code_hash = filebase64sha256(data.archive_file.lambda_source.output_path)
7+
runtime = var.lambda_runtime
8+
architectures = ["arm64"]
9+
timeout = var.timeout
10+
11+
environment {
12+
variables = {
13+
USECASE = var.usecase
14+
}
15+
}
16+
17+
vpc_config {
18+
subnet_ids = var.subnet_ids
19+
security_group_ids = [
20+
var.internet_egress_security_group_id
21+
]
22+
}
23+
}
24+

lambda_source/lambda_function.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import boto3
2+
import botocore.exceptions
3+
import logging
4+
5+
client = boto3.client('ec2')
6+
logger = logging.getLogger()
7+
logger.setLevel(logging.INFO)
8+
9+
10+
def lambda_handler(event, context):
11+
paginator = client.get_paginator('describe_network_interfaces')
12+
13+
interfaces = paginator.paginate(
14+
Filters=[
15+
{
16+
'Name': 'status',
17+
'Values': ['available']
18+
}
19+
]
20+
).build_full_result()
21+
22+
count = len(interfaces['NetworkInterfaces'])
23+
24+
x = 1
25+
for interface in interfaces['NetworkInterfaces']:
26+
description = interface.get('Description', '')
27+
if 'datasync client for' in description:
28+
logger.info(f"{x}/{count} ENI attached to DataSync task. Skipping Deletion for {interface['NetworkInterfaceId']} {interface['AvailabilityZone']} {interface['PrivateIpAddress']}")
29+
else:
30+
logger.info(f"{x}/{count} Deleting network interface {interface['NetworkInterfaceId']} {interface['AvailabilityZone']} {interface['PrivateIpAddress']}")
31+
logger.info(f"Description: {interface['Description']}")
32+
33+
try:
34+
response = client.delete_network_interface(NetworkInterfaceId=interface['NetworkInterfaceId'])
35+
logger.info(f"Response: {response}")
36+
except botocore.exceptions.ClientError as error:
37+
logger.warn(f"Delete failed: {error}")
38+
39+
x += 1

main.tf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module "iam" {
2+
source = "./iam"
3+
4+
count = var.iam_role_arn == null ? 1 : 0
5+
usecase = var.usecase
6+
account_name = var.account_name
7+
permissions_boundary_arn = var.permissions_boundary_arn
8+
}

0 commit comments

Comments
 (0)