├── .gitignore ├── Makefile ├── README.md ├── bastion.tf ├── bin └── write-mime-multipart ├── consul.tf ├── files ├── bastion │ └── cloud-config.yaml ├── common │ ├── configure-dhclient.sh │ └── install-consul.sh └── consul │ └── cloud-config.yaml ├── network.tf ├── provider.tf └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | terraform.tfvars 2 | terraform.tfstate 3 | terraform.tfstate.backup 4 | *.tfplan 5 | files/*/cloud-init.txt 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | HOSTS := bastion consul 2 | HOST_DIRS := $(addprefix files/,$(HOSTS)) 3 | TARGETS := $(addsuffix /cloud-init.txt,$(HOST_DIRS)) 4 | 5 | all: $(TARGETS) 6 | 7 | define make-goal 8 | $1/cloud-init.txt: $(wildcard files/common/*.sh) $(wildcard $1/*.yaml) $(wildcard $1/*.sh) 9 | endef 10 | 11 | $(foreach hdir,$(HOST_DIRS),$(eval $(call make-goal,$(hdir)))) 12 | 13 | $(TARGETS): 14 | $(CURDIR)/bin/write-mime-multipart --output=$@ $^ 15 | 16 | plan: $(TARGETS) 17 | terraform $@ 18 | 19 | apply: $(TARGETS) 20 | terraform $@ 21 | 22 | destroy: 23 | terraform plan -destroy -out=destroy.tfplan 24 | terraform apply destroy.tfplan 25 | 26 | clean: 27 | rm $(TARGETS) 28 | 29 | .PHONY: all plan apply clean destroy 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform AWS Test Environment 2 | 3 | This is an experimentthat creates a test environment subset in a VPC in AWS. The default region is ```us-west-2```. It creates three subnets DMZ, Public, Private and installs a bastion box in DMZ to allow access to the other subnets. It also installs a Consul cluster which is used as the DNS server for hosts within the VPC. 4 | 5 | ## Prerequisites 6 | 7 | You must have an AWS account to use these instructions. Once you have one, create an IAM user called ```terraform``` and save the access and secret keys that are given to you. Then ensure that the ```terraform``` user has the "Amazon EC2 Full Access" policy template applied either a via group or role. 8 | 9 | Now install the ```awscli``` command line tools. On OS X that can be done by ```brew install awscli```. Once the tools are installed run 10 | 11 | ```sh 12 | $ aws configure 13 | AWS Access Key ID [None]: 14 | AWS Secret Access Key [None]: 15 | Default region name [None]: us-west-2 16 | Default output format [None]: 17 | ``` 18 | 19 | When prompted for the access and secret keys, enter the ones you saved earlier. Set the default region to ```us-west-2``` and the output format can be left as default. 20 | 21 | Now install terraform (0.3.1 or later) by downloading the right binaries from http://www.terraform.io/downloads.html and extracting them on to your path somewhere. You can test things work by running ```terraform``` on the command line. 22 | 23 | To get started first create an empty directory to act as the working directory, change to it, and then initialise terraform with this module: 24 | 25 | ```sh 26 | $ terraform init github.com/deverton/terraform-aws-consul 27 | ``` 28 | 29 | You will now need to create a file in this directory called ```terraform.tfvars``` with contents like this: 30 | 31 | ``` 32 | access_key = "YOUR ACCESS KEY" 33 | secret_key = "YOUR SECRET KEY" 34 | allowed_network = "YOUR NETWORK CIDR" 35 | ``` 36 | 37 | Populate the above values with your AWS IAM keys you saved earlier and the CIDR of the network you want to allow access to the bastion host. 38 | 39 | To allow SSH access to the test VPC you must import your public key in to EC2. 40 | 41 | ```sh 42 | $ aws ec2 import-key-pair --public-key-material file://~/.ssh/id_rsa.pub --key-name terraform 43 | ``` 44 | 45 | You should then be able to apply the module. Note that this may cost you money (though not much at the moment). 46 | 47 | ```sh 48 | $ terraform apply 49 | ``` 50 | 51 | Once you have an environment running you can SSH to the bastion server as follows. The -A argument enables agent forwarding which will allow you to SSH from the bastion host to other hosts without a password. 52 | 53 | ```sh 54 | $ ssh -A ec2-user@$(terraform output bastion) 55 | ``` 56 | 57 | Note that it will take some time for the instances to actually start up and spawn the SSH service so you will get connection refused for a while, up to five minutes. Once you've got on to the box, you can prove that Consul is being used for DNS by running dig. Your output should look something like this: 58 | 59 | ```sh 60 | [ec2-user@ip-10-0-201-28 ~]$ dig consul.service.consul +noall +answer SRV 61 | 62 | ; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.23.rc1.32.amzn1 <<>> consul.service.consul +noall +answer SRV 63 | ;; global options: +cmd 64 | consul.service.consul. 0 IN SRV 1 1 8300 ip-10-0-1-11.node.dc1.consul. 65 | consul.service.consul. 0 IN SRV 1 1 8300 ip-10-0-1-12.node.dc1.consul. 66 | consul.service.consul. 0 IN SRV 1 1 8300 ip-10-0-1-10.node.dc1.consul. 67 | ``` 68 | 69 | To destroy the environment do this: 70 | 71 | ```sh 72 | $ terraform plan -destroy -out=destroy.tfplan 73 | $ terraform apply destroy.tfplan 74 | ``` 75 | 76 | Due to a bug in terraform you can't just used ```terraform destroy``` and you may find you'll need to repeat the ```apply``` command as well. 77 | 78 | ## Notes 79 | 80 | To provision the non-public facing (i.e. everything other than the bastion host) you have to use cloud-init. See the consul.tf file for an example. 81 | 82 | -------------------------------------------------------------------------------- /bastion.tf: -------------------------------------------------------------------------------- 1 | ## 2 | # Create a bastion host to allow SSH in to the test network. 3 | # Connections are only allowed from ${var.allowed_network} 4 | # This box also acts as a NAT for the private network 5 | ## 6 | 7 | resource "aws_security_group" "bastion" { 8 | name = "bastion" 9 | description = "Allow access from allowed_network to SSH/Consul, and NAT internal traffic" 10 | vpc_id = "${aws_vpc.test.id}" 11 | 12 | # SSH 13 | ingress = { 14 | from_port = 22 15 | to_port = 22 16 | protocol = "tcp" 17 | cidr_blocks = [ "${var.allowed_network}" ] 18 | self = false 19 | } 20 | 21 | # Consul 22 | ingress = { 23 | from_port = 8500 24 | to_port = 8500 25 | protocol = "tcp" 26 | cidr_blocks = [ "${var.allowed_network}" ] 27 | self = false 28 | } 29 | 30 | # NAT 31 | ingress { 32 | from_port = 0 33 | to_port = 65535 34 | protocol = "tcp" 35 | cidr_blocks = [ 36 | "${aws_subnet.public.cidr_block}", 37 | "${aws_subnet.private.cidr_block}" 38 | ] 39 | self = false 40 | } 41 | } 42 | 43 | resource "aws_security_group" "allow_bastion" { 44 | name = "allow_bastion_ssh" 45 | description = "Allow access from bastion host" 46 | vpc_id = "${aws_vpc.test.id}" 47 | ingress { 48 | from_port = 0 49 | to_port = 65535 50 | protocol = "tcp" 51 | security_groups = ["${aws_security_group.bastion.id}"] 52 | self = false 53 | } 54 | } 55 | 56 | resource "aws_instance" "bastion" { 57 | connection { 58 | user = "ec2-user" 59 | key_file = "${var.key_path}" 60 | } 61 | ami = "${lookup(var.amazon_nat_amis, var.region)}" 62 | instance_type = "t2.micro" 63 | key_name = "${var.key_name}" 64 | security_groups = [ 65 | "${aws_security_group.bastion.id}" 66 | ] 67 | subnet_id = "${aws_subnet.dmz.id}" 68 | associate_public_ip_address = true 69 | source_dest_check = false 70 | user_data = "${file(\"files/bastion/cloud-init.txt\")}" 71 | tags = { 72 | Name = "bastion" 73 | subnet = "dmz" 74 | role = "bastion" 75 | environment = "test" 76 | } 77 | } 78 | 79 | output "bastion" { 80 | value = "${aws_instance.bastion.public_ip}" 81 | } 82 | 83 | -------------------------------------------------------------------------------- /bin/write-mime-multipart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.6 2 | # largely taken from python examples 3 | # http://docs.python.org/library/email-examples.html 4 | 5 | import os 6 | import sys 7 | import smtplib 8 | # For guessing MIME type based on file name extension 9 | import mimetypes 10 | 11 | from email import encoders 12 | from email.message import Message 13 | from email.mime.base import MIMEBase 14 | from email.mime.multipart import MIMEMultipart 15 | from email.mime.text import MIMEText 16 | from optparse import OptionParser 17 | import gzip 18 | 19 | from base64 import b64encode 20 | 21 | COMMASPACE = ', ' 22 | 23 | starts_with_mappings={ 24 | '#include' : 'text/x-include-url', 25 | '#!' : 'text/x-shellscript', 26 | '#cloud-config' : 'text/cloud-config', 27 | '#upstart-job' : 'text/upstart-job', 28 | '#part-handler' : 'text/part-handler', 29 | '#cloud-boothook' : 'text/cloud-boothook' 30 | } 31 | 32 | def get_type(fname,deftype): 33 | f = file(fname,"rb") 34 | line = f.readline() 35 | f.close() 36 | rtype = deftype 37 | for str,mtype in starts_with_mappings.items(): 38 | if line.startswith(str): 39 | rtype = mtype 40 | break 41 | return(rtype) 42 | 43 | def main(): 44 | outer = MIMEMultipart() 45 | #outer['Subject'] = 'Contents of directory %s' % os.path.abspath(directory) 46 | #outer['To'] = COMMASPACE.join(opts.recipients) 47 | #outer['From'] = opts.sender 48 | #outer.preamble = 'You will not see this in a MIME-aware mail reader.\n' 49 | 50 | parser = OptionParser() 51 | 52 | parser.add_option("-o", "--output", dest="output", 53 | help="write output to FILE [default %default]", metavar="FILE", 54 | default="-") 55 | parser.add_option("-z", "--gzip", dest="compress", action="store_true", 56 | help="compress output", default=False) 57 | parser.add_option("-d", "--default", dest="deftype", 58 | help="default mime type [default %default]", default="text/plain") 59 | parser.add_option("--delim", dest="delim", 60 | help="delimiter [default %default]", default=":") 61 | parser.add_option("-b", "--base64", dest="base64", action="store_true", 62 | help="encode content base64", default=False) 63 | 64 | (options, args) = parser.parse_args() 65 | 66 | if (len(args)) < 1: 67 | parser.error("Must give file list see '--help'") 68 | 69 | for arg in args: 70 | t = arg.split(options.delim, 1) 71 | path=t[0] 72 | if len(t) > 1: 73 | mtype = t[1] 74 | else: 75 | mtype = get_type(path,options.deftype) 76 | 77 | maintype, subtype = mtype.split('/', 1) 78 | if maintype == 'text': 79 | fp = open(path) 80 | # Note: we should handle calculating the charset 81 | msg = MIMEText(fp.read(), _subtype=subtype) 82 | fp.close() 83 | else: 84 | fp = open(path, 'rb') 85 | msg = MIMEBase(maintype, subtype) 86 | msg.set_payload(fp.read()) 87 | fp.close() 88 | # Encode the payload using Base64 89 | encoders.encode_base64(msg) 90 | 91 | # Set the filename parameter 92 | msg.add_header('Content-Disposition', 'attachment', 93 | filename=os.path.basename(path)) 94 | 95 | outer.attach(msg) 96 | 97 | if options.output is "-": 98 | ofile = sys.stdout 99 | else: 100 | ofile = file(options.output,"wb") 101 | 102 | if options.base64: 103 | output = b64encode(outer.as_string()) 104 | else: 105 | output = outer.as_string() 106 | 107 | if options.compress: 108 | gfile = gzip.GzipFile(fileobj=ofile, filename = options.output ) 109 | gfile.write(output) 110 | gfile.close() 111 | else: 112 | ofile.write(output) 113 | 114 | ofile.close() 115 | 116 | if __name__ == '__main__': 117 | main() 118 | 119 | -------------------------------------------------------------------------------- /consul.tf: -------------------------------------------------------------------------------- 1 | ## 2 | # Consul cluster setup 3 | ## 4 | 5 | resource "aws_security_group" "consul" { 6 | name = "consul" 7 | description = "Consul internal traffic + maintenance." 8 | vpc_id = "${aws_vpc.test.id}" 9 | 10 | ingress { 11 | from_port = 53 12 | to_port = 53 13 | protocol = "tcp" 14 | self = true 15 | } 16 | ingress { 17 | from_port = 53 18 | to_port = 53 19 | protocol = "udp" 20 | self = true 21 | } 22 | ingress { 23 | from_port = 8300 24 | to_port = 8302 25 | protocol = "tcp" 26 | self = true 27 | } 28 | ingress { 29 | from_port = 8301 30 | to_port = 8302 31 | protocol = "udp" 32 | self = true 33 | } 34 | ingress { 35 | from_port = 8400 36 | to_port = 8400 37 | protocol = "tcp" 38 | self = true 39 | } 40 | ingress { 41 | from_port = 8500 42 | to_port = 8500 43 | protocol = "tcp" 44 | self = true 45 | } 46 | } 47 | 48 | resource "aws_instance" "consul" { 49 | depends_on = [ "aws_instance.bastion" ] 50 | connection { 51 | user = "ec2-user" 52 | key_file = "${var.key_path}" 53 | } 54 | ami = "${lookup(var.amazon_amis, var.region)}" 55 | instance_type = "t2.micro" 56 | count = 3 57 | key_name = "${var.key_name}" 58 | security_groups = [ 59 | "${aws_security_group.allow_bastion.id}", 60 | "${aws_security_group.consul.id}" 61 | ] 62 | subnet_id = "${aws_subnet.private.id}" 63 | private_ip = "10.0.1.1${count.index}" 64 | tags = { 65 | Name = "consul${count.index}" 66 | subnet = "private" 67 | role = "dns" 68 | environment = "test" 69 | } 70 | user_data = "${file(\"files/consul/cloud-init.txt\")}" 71 | } 72 | 73 | -------------------------------------------------------------------------------- /files/bastion/cloud-config.yaml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | write_files: 3 | - path: /etc/terraform_environment 4 | content: | 5 | ROLE="bastion consul-ui" 6 | 7 | -------------------------------------------------------------------------------- /files/common/configure-dhclient.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Using Consul in dhclient..." 5 | cat >/etc/dhcp/dhclient.conf << EOF 6 | timeout 300; 7 | supersede domain-name "node.dc1.consul"; 8 | supersede domain-search "service.dc1.consul", "node.dc1.consul"; 9 | supersede domain-name-servers 127.0.0.1, 10.0.0.2; 10 | EOF 11 | chmod 0644 /etc/dhcp/dhclient.conf 12 | 13 | service network reload 14 | 15 | -------------------------------------------------------------------------------- /files/common/install-consul.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | source /etc/terraform_environment 5 | 6 | SERVER_ARGS="" 7 | UI_DIR="null" 8 | HTTP_CLIENT_ADDR="127.0.0.1" 9 | 10 | echo "Installing Consul..." 11 | pushd /tmp 12 | wget https://dl.bintray.com/mitchellh/consul/0.4.1_linux_amd64.zip -O consul.zip 13 | unzip consul.zip >/dev/null 14 | chmod +x consul 15 | mv consul /usr/local/bin/consul 16 | mkdir -p /etc/consul.d 17 | mkdir -p /mnt/consul/data 18 | mkdir -p /etc/service 19 | rm /tmp/consul.zip 20 | popd 21 | 22 | if [[ "${ROLE}" == *consul-server* ]]; then 23 | echo "Configure as Consul Server..." 24 | 25 | SERVER_ARGS="-server -bootstrap-expect=3" 26 | else 27 | echo "Configure as Consul Client..." 28 | fi 29 | 30 | if [[ "${ROLE}" == *consul-ui* ]]; then 31 | echo "Installing Consul UI..." 32 | pushd /tmp 33 | wget https://dl.bintray.com/mitchellh/consul/0.4.1_web_ui.zip -O consul-ui.zip 34 | unzip consul-ui.zip >/dev/null 35 | mkdir -p /mnt/consul/ui 36 | mv dist/* /mnt/consul/ui/ 37 | rm /tmp/consul-ui.zip 38 | popd 39 | 40 | HTTP_CLIENT_ADDR="0.0.0.0" 41 | UI_DIR="\"/mnt/consul/ui\"" 42 | fi 43 | 44 | # Configuration file 45 | echo "Creating configuration..." 46 | cat >/etc/consul.d/config.json << EOF 47 | { 48 | "addresses" : { 49 | "http" : "${HTTP_CLIENT_ADDR}" 50 | }, 51 | "ports" : { 52 | "dns" : 53 53 | }, 54 | "recursor" : "10.0.0.2", 55 | "disable_anonymous_signature" : true, 56 | "disable_update_check" : true, 57 | "data_dir" : "/mnt/consul/data", 58 | "ui_dir" : $UI_DIR 59 | } 60 | EOF 61 | chmod 0644 /etc/consul.d/config.json 62 | 63 | # Setup the join address 64 | echo "Configure IPs..." 65 | cat >/etc/service/consul-join << EOF 66 | export CONSUL_JOIN="10.0.1.10 10.0.1.11 10.0.1.12" 67 | EOF 68 | chmod 0644 /etc/service/consul-join 69 | 70 | # Configure the server 71 | echo "Configure server..." 72 | cat >/etc/service/consul << EOF 73 | export CONSUL_FLAGS="${SERVER_ARGS}" 74 | EOF 75 | chmod 0644 /etc/service/consul 76 | 77 | # Add "first start" join service 78 | echo "Creating 'join' service..." 79 | cat >/etc/init/consul-join.conf <<"EOF" 80 | description "Join the consul cluster" 81 | 82 | start on started consul 83 | stop on stopped consul 84 | 85 | task 86 | 87 | script 88 | if [ -f "/etc/service/consul-join" ]; then 89 | . /etc/service/consul-join 90 | fi 91 | 92 | # Keep trying to join until it succeeds 93 | set +e 94 | while :; do 95 | logger -t "consul-join" "Attempting join: ${CONSUL_JOIN}" 96 | /usr/local/bin/consul join \ 97 | ${CONSUL_JOIN} \ 98 | >>/var/log/consul-join.log 2>&1 99 | [ $? -eq 0 ] && break 100 | sleep 5 101 | done 102 | 103 | logger -t "consul-join" "Join success!" 104 | end script 105 | EOF 106 | chmod 0644 /etc/init/consul-join.conf 107 | 108 | # Add actual service 109 | echo "Creating service..." 110 | cat >/etc/init/consul.conf <<"EOF" 111 | description "Consul agent" 112 | 113 | start on runlevel [2345] 114 | stop on runlevel [!2345] 115 | 116 | respawn 117 | 118 | script 119 | if [ -f "/etc/service/consul" ]; then 120 | . /etc/service/consul 121 | fi 122 | 123 | # Make sure to use all our CPUs, because Consul can block a scheduler thread 124 | export GOMAXPROCS=`nproc` 125 | 126 | exec /usr/local/bin/consul agent \ 127 | -config-dir="/etc/consul.d" \ 128 | ${CONSUL_FLAGS} \ 129 | >>/var/log/consul.log 2>&1 130 | end script 131 | EOF 132 | chmod 0644 /etc/init/consul.conf 133 | 134 | # Start service 135 | echo "Starting service..." 136 | initctl start consul 137 | 138 | -------------------------------------------------------------------------------- /files/consul/cloud-config.yaml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | write_files: 3 | - path: /etc/terraform_environment 4 | content: | 5 | ROLE="consul-server" 6 | 7 | -------------------------------------------------------------------------------- /network.tf: -------------------------------------------------------------------------------- 1 | ## 2 | # VPC 3 | ## 4 | resource "aws_vpc" "test" { 5 | cidr_block = "10.0.0.0/16" 6 | } 7 | 8 | resource "aws_internet_gateway" "gateway" { 9 | vpc_id = "${aws_vpc.test.id}" 10 | } 11 | 12 | ## 13 | # DMZ 14 | ## 15 | 16 | resource "aws_subnet" "dmz" { 17 | vpc_id = "${aws_vpc.test.id}" 18 | cidr_block = "10.0.201.0/24" 19 | } 20 | 21 | resource "aws_route_table" "dmz" { 22 | vpc_id = "${aws_vpc.test.id}" 23 | route { 24 | cidr_block = "0.0.0.0/0" 25 | gateway_id = "${aws_internet_gateway.gateway.id}" 26 | } 27 | } 28 | 29 | resource "aws_route_table_association" "dmz" { 30 | subnet_id = "${aws_subnet.dmz.id}" 31 | route_table_id = "${aws_route_table.dmz.id}" 32 | } 33 | 34 | ## 35 | # Public 36 | ## 37 | 38 | resource "aws_subnet" "public" { 39 | vpc_id = "${aws_vpc.test.id}" 40 | cidr_block = "10.0.0.0/24" 41 | } 42 | 43 | resource "aws_route_table" "public" { 44 | vpc_id = "${aws_vpc.test.id}" 45 | route { 46 | cidr_block = "0.0.0.0/0" 47 | instance_id = "${aws_instance.bastion.id}" 48 | } 49 | } 50 | 51 | resource "aws_route_table_association" "public" { 52 | subnet_id = "${aws_subnet.public.id}" 53 | route_table_id = "${aws_route_table.public.id}" 54 | } 55 | 56 | ## 57 | # Private 58 | ## 59 | 60 | resource "aws_subnet" "private" { 61 | vpc_id = "${aws_vpc.test.id}" 62 | cidr_block = "10.0.1.0/24" 63 | } 64 | 65 | resource "aws_route_table" "private" { 66 | vpc_id = "${aws_vpc.test.id}" 67 | route { 68 | cidr_block = "0.0.0.0/0" 69 | instance_id = "${aws_instance.bastion.id}" 70 | } 71 | } 72 | 73 | resource "aws_route_table_association" "private" { 74 | subnet_id = "${aws_subnet.private.id}" 75 | route_table_id = "${aws_route_table.private.id}" 76 | } 77 | 78 | -------------------------------------------------------------------------------- /provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | access_key = "${var.access_key}" 3 | secret_key = "${var.secret_key}" 4 | region = "${var.region}" 5 | } 6 | 7 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "access_key" { 2 | description = "AWS access key." 3 | } 4 | 5 | variable "secret_key" { 6 | description = "AWS secret key." 7 | } 8 | 9 | variable "allowed_network" { 10 | description = "The CIDR of network that is allowed to access the bastion host" 11 | } 12 | 13 | variable "region" { 14 | description = "The AWS region to create things in." 15 | default = "us-west-2" 16 | } 17 | 18 | variable "key_name" { 19 | description = "Name of the keypair to use in EC2." 20 | default = "terraform" 21 | } 22 | 23 | variable "key_path" { 24 | description = "Path to your private key." 25 | default = "~/.ssh/id_rsa" 26 | } 27 | 28 | variable "amazon_amis" { 29 | description = "Amazon Linux AMIs" 30 | default = { 31 | us-west-2 = "ami-b5a7ea85" 32 | } 33 | } 34 | 35 | variable "amazon_nat_amis" { 36 | description = "Amazon Linux NAT AMIs" 37 | default = { 38 | us-west-2 = "ami-bb69128b" 39 | } 40 | } 41 | 42 | variable "centos7_amis" { 43 | description = "CentOS 7 AMIs" 44 | default = { 45 | us-east-1 = "ami-96a818fe" 46 | us-west-2 = "ami-c7d092f7" 47 | us-west-1 = "ami-6bcfc42e" 48 | eu-west-1 = "ami-e4ff5c93" 49 | ap-southeast-1 = "ami-aea582fc" 50 | ap-southeast-2 = "ami-bd523087" 51 | ap-northeast-1 = "ami-89634988" 52 | sa-east-1 = "ami-bf9520a2" 53 | } 54 | } 55 | 56 | --------------------------------------------------------------------------------