Skip to content
This repository was archived by the owner on Jul 20, 2024. It is now read-only.

Commit e717e30

Browse files
authored
Add user_data_write_files and user_data_runcmd (#19)
1 parent de153d3 commit e717e30

File tree

8 files changed

+234
-72
lines changed

8 files changed

+234
-72
lines changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,29 @@ The instance will run [init.sh](data/init.sh) to enable NAT as follows:
7575

7676
## Configuration
7777

78-
### Allow SSH access
78+
### User data
7979

80-
You can log in to the NAT instance from [AWS Systems Manager Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html).
80+
You can set additional `write_files` and `runcmd` section. For example,
81+
82+
```tf
83+
module "nat" {
84+
user_data_write_files = [
85+
{
86+
path : "/opt/nat/run.sh",
87+
content : file("./run.sh"),
88+
permissions : "0755",
89+
},
90+
]
91+
user_data_runcmd = [
92+
["/opt/nat/run.sh"],
93+
]
94+
}
95+
```
96+
97+
See also the [example](example/) for more.
98+
99+
100+
### SSH access
81101

82102
You can enable SSH access by setting `key_name` option and opening the security group. For example,
83103

@@ -127,6 +147,8 @@ No requirements.
127147
| public\_subnet | ID of the public subnet to place the NAT instance | `string` | n/a | yes |
128148
| tags | Tags applied to resources created with this module | `map` | `{}` | no |
129149
| use\_spot\_instance | Whether to use spot or on-demand EC2 instance | `bool` | `true` | no |
150+
| user\_data\_runcmd | Additional runcmd section of cloud-init | `list` | `[]` | no |
151+
| user\_data\_write\_files | Additional write\_files section of cloud-init | `list` | `[]` | no |
130152
| vpc\_id | ID of the VPC | `string` | n/a | yes |
131153

132154
## Outputs

example/README.md

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,39 @@
11
# Example of terraform-aws-nat-instance
22

3+
This example shows the following things:
4+
5+
- Create a VPC and subnets using `vpc` module.
6+
- Create a NAT instance using this module.
7+
- Create an instance in the private subnet.
8+
- Add custom scripts to the NAT instance.
9+
In this example, http port of the private instance will be exposed.
10+
11+
12+
## Getting Started
13+
314
Provision the stack.
415

516
```console
617
% terraform init
7-
818
% terraform apply
919
...
10-
Plan: 37 to add, 0 to change, 0 to destroy.
1120

12-
Do you want to perform these actions?
13-
Terraform will perform the actions described above.
14-
Only 'yes' will be accepted to approve.
21+
Outputs:
1522

16-
Enter a value: yes
17-
...
18-
Apply complete! Resources: 37 added, 0 changed, 0 destroyed.
23+
nat_public_ip = 54.212.155.23
24+
private_instance_id = i-07c076946c5142cdd
25+
```
26+
27+
Make sure you have access to the instance in the private subnet.
28+
29+
```console
30+
% aws ssm start-session --region us-west-2 --target i-07c076946c5142cdd
1931
```
2032

21-
Make sure you can access an instance in the private subnet.
33+
Make sure you can access http port of the NAT instance.
2234

2335
```console
24-
% aws ssm start-session --region us-west-2 --target i-01d945b895167862a
36+
% curl http://54.212.155.23
2537
```
2638

2739
You can completely destroy the stack.

example/dnat.service

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[Unit]
2+
Description = DNAT via ENI eth1
3+
4+
[Service]
5+
ExecStart = /opt/nat/dnat.sh
6+
Type = simple
7+
Restart = always
8+
9+
[Install]
10+
WantedBy = multi-user.target

example/dnat.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash -x
2+
3+
region="$(/opt/aws/bin/ec2-metadata -z | sed 's/placement: \(.*\).$/\1/')"
4+
eth1_addr="$(ip -f inet -o addr show dev eth1 | cut -d' ' -f 7 | cut -d/ -f 1)"
5+
6+
function get_instance_private_ip_by_name() {
7+
local name="$1"
8+
aws ec2 describe-instances \
9+
--region "$region" \
10+
--filters "Name=tag:Name,Values=$name" "Name=instance-state-name,Values=running" |
11+
jq -r .Reservations[0].Instances[0].PrivateIpAddress
12+
}
13+
14+
function run_iptables() {
15+
local action="$1"
16+
iptables -t nat "$action" PREROUTING 1 -m tcp -p tcp \
17+
--dst "$eth1_addr" --dport 80 \
18+
-j DNAT --to-destination "$(get_instance_private_ip_by_name ${ec2_name}):80"
19+
}
20+
21+
run_iptables -I
22+
while true; do
23+
sleep 30
24+
run_iptables -R
25+
done

example/example.tf

Lines changed: 44 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,68 +21,56 @@ module "nat" {
2121
public_subnet = module.vpc.public_subnets[0]
2222
private_subnets_cidr_blocks = module.vpc.private_subnets_cidr_blocks
2323
private_route_table_ids = module.vpc.private_route_table_ids
24-
}
25-
26-
# instance in the private subnet
27-
resource "aws_instance" "private_instance" {
28-
ami = data.aws_ami.amazon_linux_2.id
29-
instance_type = "t3.micro"
30-
iam_instance_profile = aws_iam_instance_profile.private_instance.name
31-
subnet_id = module.vpc.private_subnets[0]
32-
33-
tags = {
34-
Name = "Example of terraform-aws-nat-instance"
35-
}
36-
}
3724

38-
# AMI of the latest Amazon Linux 2
39-
data "aws_ami" "amazon_linux_2" {
40-
most_recent = true
41-
owners = ["amazon"]
42-
filter {
43-
name = "architecture"
44-
values = ["x86_64"]
45-
}
46-
filter {
47-
name = "root-device-type"
48-
values = ["ebs"]
49-
}
50-
filter {
51-
name = "name"
52-
values = ["amzn2-ami-hvm-*"]
53-
}
54-
filter {
55-
name = "virtualization-type"
56-
values = ["hvm"]
57-
}
58-
filter {
59-
name = "block-device-mapping.volume-type"
60-
values = ["gp2"]
61-
}
62-
}
63-
64-
resource "aws_iam_instance_profile" "private_instance" {
65-
role = aws_iam_role.private_instance.name
25+
# enable port forwarding (optional)
26+
user_data_write_files = [
27+
{
28+
path : "/opt/nat/dnat.sh",
29+
content : templatefile("./dnat.sh", { ec2_name = "example-terraform-aws-nat-instance" }),
30+
permissions : "0755",
31+
},
32+
{
33+
path : "/etc/systemd/system/dnat.service",
34+
content : file("./dnat.service"),
35+
},
36+
]
37+
user_data_runcmd = [
38+
["yum", "install", "-y", "jq"],
39+
["systemctl", "enable", "dnat"],
40+
["systemctl", "start", "dnat"],
41+
]
6642
}
6743

68-
resource "aws_iam_role" "private_instance" {
69-
assume_role_policy = <<EOF
44+
# IAM policy for port forwarding (optional)
45+
resource "aws_iam_role_policy" "dnat_service" {
46+
role = module.nat.iam_role_name
47+
policy = <<EOF
7048
{
71-
"Version": "2012-10-17",
72-
"Statement": [
73-
{
74-
"Effect": "Allow",
75-
"Principal": {
76-
"Service": "ec2.amazonaws.com"
77-
},
78-
"Action": "sts:AssumeRole"
79-
}
80-
]
49+
"Version": "2012-10-17",
50+
"Statement": [
51+
{
52+
"Effect": "Allow",
53+
"Action": [
54+
"ec2:DescribeInstances"
55+
],
56+
"Resource": "*"
57+
}
58+
]
8159
}
8260
EOF
8361
}
8462

85-
resource "aws_iam_role_policy_attachment" "ssm" {
86-
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
87-
role = aws_iam_role.private_instance.name
63+
# expose http port of the private instance (optional)
64+
resource "aws_security_group_rule" "dnat_http" {
65+
description = "expose HTTP service"
66+
security_group_id = module.nat.sg_id
67+
type = "ingress"
68+
protocol = "tcp"
69+
from_port = 80
70+
to_port = 80
71+
cidr_blocks = ["0.0.0.0/0"]
72+
}
73+
74+
output "nat_public_ip" {
75+
value = module.nat.eip_public_ip
8876
}

example/instance.tf

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# an example instance in the private subnet
2+
resource "aws_instance" "private_instance" {
3+
ami = data.aws_ami.amazon_linux_2.id
4+
instance_type = "t3.micro"
5+
iam_instance_profile = aws_iam_instance_profile.private_instance.name
6+
subnet_id = module.vpc.private_subnets[0]
7+
vpc_security_group_ids = [aws_security_group.private_instance.id]
8+
9+
tags = {
10+
Name = "example-terraform-aws-nat-instance"
11+
}
12+
13+
user_data = <<EOF
14+
#!/bin/bash
15+
yum install -y httpd
16+
systemctl start httpd
17+
EOF
18+
}
19+
20+
resource "aws_security_group" "private_instance" {
21+
name = "example-terraform-aws-nat-instance"
22+
description = "expose http service"
23+
vpc_id = module.vpc.vpc_id
24+
ingress {
25+
protocol = "tcp"
26+
from_port = 80
27+
to_port = 80
28+
security_groups = [module.nat.sg_id]
29+
}
30+
egress {
31+
protocol = "-1"
32+
from_port = 0
33+
to_port = 0
34+
cidr_blocks = ["0.0.0.0/0"]
35+
}
36+
}
37+
38+
# AMI of the latest Amazon Linux 2
39+
data "aws_ami" "amazon_linux_2" {
40+
most_recent = true
41+
owners = ["amazon"]
42+
filter {
43+
name = "architecture"
44+
values = ["x86_64"]
45+
}
46+
filter {
47+
name = "root-device-type"
48+
values = ["ebs"]
49+
}
50+
filter {
51+
name = "name"
52+
values = ["amzn2-ami-hvm-*"]
53+
}
54+
filter {
55+
name = "virtualization-type"
56+
values = ["hvm"]
57+
}
58+
filter {
59+
name = "block-device-mapping.volume-type"
60+
values = ["gp2"]
61+
}
62+
}
63+
64+
# enable SSM access
65+
resource "aws_iam_instance_profile" "private_instance" {
66+
role = aws_iam_role.private_instance.name
67+
}
68+
69+
resource "aws_iam_role" "private_instance" {
70+
assume_role_policy = <<EOF
71+
{
72+
"Version": "2012-10-17",
73+
"Statement": [
74+
{
75+
"Effect": "Allow",
76+
"Principal": {
77+
"Service": "ec2.amazonaws.com"
78+
},
79+
"Action": "sts:AssumeRole"
80+
}
81+
]
82+
}
83+
EOF
84+
}
85+
86+
resource "aws_iam_role_policy_attachment" "ssm" {
87+
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
88+
role = aws_iam_role.private_instance.name
89+
}
90+
91+
output "private_instance_id" {
92+
value = aws_instance.private_instance.id
93+
}

main.tf

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ resource "aws_launch_template" "this" {
8989
"#cloud-config",
9090
yamlencode({
9191
# https://cloudinit.readthedocs.io/en/latest/topics/modules.html
92-
write_files : [
92+
write_files : concat([
9393
{
9494
path : "/opt/nat/runonce.sh",
9595
content : templatefile("${path.module}/runonce.sh", { eni_id = aws_network_interface.this.id }),
@@ -104,10 +104,10 @@ resource "aws_launch_template" "this" {
104104
path : "/etc/systemd/system/snat.service",
105105
content : file("${path.module}/snat.service"),
106106
},
107-
],
108-
runcmd : [
107+
], var.user_data_write_files),
108+
runcmd : concat([
109109
["/opt/nat/runonce.sh"],
110-
],
110+
], var.user_data_runcmd),
111111
})
112112
]))
113113

variables.tf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ variable "tags" {
6060
default = {}
6161
}
6262

63+
variable "user_data_write_files" {
64+
description = "Additional write_files section of cloud-init"
65+
type = list
66+
default = []
67+
}
68+
69+
variable "user_data_runcmd" {
70+
description = "Additional runcmd section of cloud-init"
71+
type = list
72+
default = []
73+
}
74+
6375
locals {
6476
// Generate common tags by merging variables and default Name
6577
common_tags = merge(

0 commit comments

Comments
 (0)