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

Commit

Permalink
Add user_data_write_files and user_data_runcmd (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
int128 authored Jun 1, 2020
1 parent de153d3 commit e717e30
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 72 deletions.
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,29 @@ The instance will run [init.sh](data/init.sh) to enable NAT as follows:

## Configuration

### Allow SSH access
### User data

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).
You can set additional `write_files` and `runcmd` section. For example,

```tf
module "nat" {
user_data_write_files = [
{
path : "/opt/nat/run.sh",
content : file("./run.sh"),
permissions : "0755",
},
]
user_data_runcmd = [
["/opt/nat/run.sh"],
]
}
```

See also the [example](example/) for more.


### SSH access

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

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

## Outputs
Expand Down
32 changes: 22 additions & 10 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
# Example of terraform-aws-nat-instance

This example shows the following things:

- Create a VPC and subnets using `vpc` module.
- Create a NAT instance using this module.
- Create an instance in the private subnet.
- Add custom scripts to the NAT instance.
In this example, http port of the private instance will be exposed.


## Getting Started

Provision the stack.

```console
% terraform init

% terraform apply
...
Plan: 37 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Outputs:

Enter a value: yes
...
Apply complete! Resources: 37 added, 0 changed, 0 destroyed.
nat_public_ip = 54.212.155.23
private_instance_id = i-07c076946c5142cdd
```

Make sure you have access to the instance in the private subnet.

```console
% aws ssm start-session --region us-west-2 --target i-07c076946c5142cdd
```

Make sure you can access an instance in the private subnet.
Make sure you can access http port of the NAT instance.

```console
% aws ssm start-session --region us-west-2 --target i-01d945b895167862a
% curl http://54.212.155.23
```

You can completely destroy the stack.
Expand Down
10 changes: 10 additions & 0 deletions example/dnat.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description = DNAT via ENI eth1

[Service]
ExecStart = /opt/nat/dnat.sh
Type = simple
Restart = always

[Install]
WantedBy = multi-user.target
25 changes: 25 additions & 0 deletions example/dnat.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash -x

region="$(/opt/aws/bin/ec2-metadata -z | sed 's/placement: \(.*\).$/\1/')"
eth1_addr="$(ip -f inet -o addr show dev eth1 | cut -d' ' -f 7 | cut -d/ -f 1)"

function get_instance_private_ip_by_name() {
local name="$1"
aws ec2 describe-instances \
--region "$region" \
--filters "Name=tag:Name,Values=$name" "Name=instance-state-name,Values=running" |
jq -r .Reservations[0].Instances[0].PrivateIpAddress
}

function run_iptables() {
local action="$1"
iptables -t nat "$action" PREROUTING 1 -m tcp -p tcp \
--dst "$eth1_addr" --dport 80 \
-j DNAT --to-destination "$(get_instance_private_ip_by_name ${ec2_name}):80"
}

run_iptables -I
while true; do
sleep 30
run_iptables -R
done
100 changes: 44 additions & 56 deletions example/example.tf
Original file line number Diff line number Diff line change
Expand Up @@ -21,68 +21,56 @@ module "nat" {
public_subnet = module.vpc.public_subnets[0]
private_subnets_cidr_blocks = module.vpc.private_subnets_cidr_blocks
private_route_table_ids = module.vpc.private_route_table_ids
}

# instance in the private subnet
resource "aws_instance" "private_instance" {
ami = data.aws_ami.amazon_linux_2.id
instance_type = "t3.micro"
iam_instance_profile = aws_iam_instance_profile.private_instance.name
subnet_id = module.vpc.private_subnets[0]

tags = {
Name = "Example of terraform-aws-nat-instance"
}
}

# AMI of the latest Amazon Linux 2
data "aws_ami" "amazon_linux_2" {
most_recent = true
owners = ["amazon"]
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
filter {
name = "name"
values = ["amzn2-ami-hvm-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
filter {
name = "block-device-mapping.volume-type"
values = ["gp2"]
}
}

resource "aws_iam_instance_profile" "private_instance" {
role = aws_iam_role.private_instance.name
# enable port forwarding (optional)
user_data_write_files = [
{
path : "/opt/nat/dnat.sh",
content : templatefile("./dnat.sh", { ec2_name = "example-terraform-aws-nat-instance" }),
permissions : "0755",
},
{
path : "/etc/systemd/system/dnat.service",
content : file("./dnat.service"),
},
]
user_data_runcmd = [
["yum", "install", "-y", "jq"],
["systemctl", "enable", "dnat"],
["systemctl", "start", "dnat"],
]
}

resource "aws_iam_role" "private_instance" {
assume_role_policy = <<EOF
# IAM policy for port forwarding (optional)
resource "aws_iam_role_policy" "dnat_service" {
role = module.nat.iam_role_name
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances"
],
"Resource": "*"
}
]
}
EOF
}

resource "aws_iam_role_policy_attachment" "ssm" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.private_instance.name
# expose http port of the private instance (optional)
resource "aws_security_group_rule" "dnat_http" {
description = "expose HTTP service"
security_group_id = module.nat.sg_id
type = "ingress"
protocol = "tcp"
from_port = 80
to_port = 80
cidr_blocks = ["0.0.0.0/0"]
}

output "nat_public_ip" {
value = module.nat.eip_public_ip
}
93 changes: 93 additions & 0 deletions example/instance.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# an example instance in the private subnet
resource "aws_instance" "private_instance" {
ami = data.aws_ami.amazon_linux_2.id
instance_type = "t3.micro"
iam_instance_profile = aws_iam_instance_profile.private_instance.name
subnet_id = module.vpc.private_subnets[0]
vpc_security_group_ids = [aws_security_group.private_instance.id]

tags = {
Name = "example-terraform-aws-nat-instance"
}

user_data = <<EOF
#!/bin/bash
yum install -y httpd
systemctl start httpd
EOF
}

resource "aws_security_group" "private_instance" {
name = "example-terraform-aws-nat-instance"
description = "expose http service"
vpc_id = module.vpc.vpc_id
ingress {
protocol = "tcp"
from_port = 80
to_port = 80
security_groups = [module.nat.sg_id]
}
egress {
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
}

# AMI of the latest Amazon Linux 2
data "aws_ami" "amazon_linux_2" {
most_recent = true
owners = ["amazon"]
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
filter {
name = "name"
values = ["amzn2-ami-hvm-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
filter {
name = "block-device-mapping.volume-type"
values = ["gp2"]
}
}

# enable SSM access
resource "aws_iam_instance_profile" "private_instance" {
role = aws_iam_role.private_instance.name
}

resource "aws_iam_role" "private_instance" {
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}

resource "aws_iam_role_policy_attachment" "ssm" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.private_instance.name
}

output "private_instance_id" {
value = aws_instance.private_instance.id
}
8 changes: 4 additions & 4 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ resource "aws_launch_template" "this" {
"#cloud-config",
yamlencode({
# https://cloudinit.readthedocs.io/en/latest/topics/modules.html
write_files : [
write_files : concat([
{
path : "/opt/nat/runonce.sh",
content : templatefile("${path.module}/runonce.sh", { eni_id = aws_network_interface.this.id }),
Expand All @@ -104,10 +104,10 @@ resource "aws_launch_template" "this" {
path : "/etc/systemd/system/snat.service",
content : file("${path.module}/snat.service"),
},
],
runcmd : [
], var.user_data_write_files),
runcmd : concat([
["/opt/nat/runonce.sh"],
],
], var.user_data_runcmd),
})
]))

Expand Down
12 changes: 12 additions & 0 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ variable "tags" {
default = {}
}

variable "user_data_write_files" {
description = "Additional write_files section of cloud-init"
type = list
default = []
}

variable "user_data_runcmd" {
description = "Additional runcmd section of cloud-init"
type = list
default = []
}

locals {
// Generate common tags by merging variables and default Name
common_tags = merge(
Expand Down

0 comments on commit e717e30

Please sign in to comment.