diff --git a/CHANGELOG.md b/CHANGELOG.md index 474ff342..b7a97ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Examples * `ad-ec2`: Demonstrate how an Windows EC2 instance seamlessly joins an Active directory when it gets newly spawned. +* `ad-linux-asg`: Demonstrate how an Linux EC2 instance (from an ASG) joins an Active directory when it gets newly spawned. # v0.9.3 diff --git a/examples/ad-linux-asg/Makefile b/examples/ad-linux-asg/Makefile new file mode 100644 index 00000000..ec854caf --- /dev/null +++ b/examples/ad-linux-asg/Makefile @@ -0,0 +1,74 @@ +.PHONY: init ssh-key plan-vpc plan-subnets plan-gateway plan apply destroy clean + +.DEFAULT_GOAL = help + +# Hardcoding value of 3 minutes when we check if the plan file is stale +STALE_PLAN_FILE := `find "tf.out" -mmin -3 | grep -q tf.out` + +## Check if tf.out is stale (Older than 2 minutes) +check-plan-file: + @if ! ${STALE_PLAN_FILE} ; then \ + echo "ERROR: Stale tf.out plan file (older than 3 minutes)!"; \ + exit 1; \ + fi + +## Runs terraform get and terraform init for env +init: + @terraform get + @terraform init + +## Create ssh key +ssh-key: + @ssh-keygen -q -N "" -b 4096 -C "SSH key for vpc-scenario-1 example" -f ./id_rsa + +## use 'terraform plan' to 'target' the vpc in the vpc module +plan-vpc: + @terraform plan \ + -target="module.vpc.module.vpc" \ + -out=tf.out + +## use 'terraform plan' to 'target' the public subnets in the vpc module +plan-subnets: + @terraform plan \ + -target="module.vpc.module.public-subnets" \ + -out=tf.out + +## use 'terraform plan' to 'target' the public gateway in the vpc module +plan-gateway: + @terraform plan \ + -target="module.vpc.module.public-gateway" \ + -out=tf.out + +## use 'terraform plan' to map out updates to apply +plan: + @terraform plan -out=tf.out + +## use 'terraform apply' to apply updates in a 'tf.out' plan file +apply: check-plan-file + @terraform apply tf.out + +## use 'terraform destroy' to remove all resources from AWS +destroy: + @terraform destroy + +## rm -rf all files and state +clean: + @rm -f tf.out + @rm -f id_rsa + @rm -f id_rsa.pub + @rm -f terraform.tfvars + @rm -f terraform.*.backup + @rm -f terraform.tfstate + +## Show help screen. +help: + @echo "Please use \`make ' where is one of\n\n" + @awk '/^[a-zA-Z\-\_0-9]+:/ { \ + helpMessage = match(lastLine, /^## (.*)/); \ + if (helpMessage) { \ + helpCommand = substr($$1, 0, index($$1, ":")); \ + helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ + printf "%-30s %s\n", helpCommand, helpMessage; \ + } \ + } \ + { lastLine = $$0 }' $(MAKEFILE_LIST) diff --git a/examples/ad-linux-asg/README.md b/examples/ad-linux-asg/README.md new file mode 100644 index 00000000..0161df63 --- /dev/null +++ b/examples/ad-linux-asg/README.md @@ -0,0 +1,130 @@ +# Active Directory with Linux EC2 join (from ASG) + +The terraform code is built on top of +[vpc-scenario1](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario1.html) +with two additional private subnets and a NAT gateway on a public +subnet. This example demonstrate how an Linux EC2 instance present in +[ASG](https://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html) +joins an Active directory when it gets newly spawned. The only +difference between this example and the [ad-asg](../ad-asg/) is +operating system. + +We use a custom AMI to have active directory related softwares +available in our instance. + +## AMI Creation + +``` shellsession +$ cd packer +$ make build +.... + amazon-ebs: Stopping instance +==> amazon-ebs: Waiting for the instance to stop... +==> amazon-ebs: Creating AMI centos7-server-ami-1569914218 from instance i-074b5b3a18dde102b + amazon-ebs: AMI: ami-0473c8e0ea159ff34 +==> amazon-ebs: Waiting for AMI to become ready... +==> amazon-ebs: Terminating the source AWS instance... +==> amazon-ebs: Cleaning up any extra volumes... +==> amazon-ebs: Destroying volume (vol-0ba5fc0a8d3e4d180)... +==> amazon-ebs: Deleting temporary security group... +==> amazon-ebs: Deleting temporary keypair... +Build 'amazon-ebs' finished. + +==> Builds finished. The artifacts of successful builds are: +--> amazon-ebs: AMIs were created: +us-east-2: ami-0473c8e0ea159ff34 +``` + +Note the new ami-id built from the above command. You would need to +put that in the [variables.tf](./variables.tf) for the `ami_id` +variable. + +## Environment creation and deployment + +To use this example set up AWS credentials and then run the commands in the +following order: + +``` +make ssh-key +make init +make plan-vpc +make apply +make plan-subnets +make apply +make plan-gateway +make apply +make plan +make apply +``` + +## Execution + +Once you run the above commands, you will get an output like this: + +``` shellsession +... +module.nat-gateway.aws_route_table_association.private-rta[0]: Refreshing state... [id=rtbassoc-0be4f2c71ef12e768] +module.nat-gateway.aws_route_table_association.private-rta[1]: Refreshing state... [id=rtbassoc-08a1f878abab73841] +aws_ssm_association.associate_ssm: Refreshing state... [id=996ff9a8-0931-4000-85aa-d01ef536f5a7] + + +Outputs: + +asg-name = test-ad-project-asg-cluster20190919093341776000000005 +microsoft-ad_dns_ip_addresses = [ + "10.23.21.134", + "10.23.22.45", +] +microsoft-ad_dns_name = dev.fpcomplete.local +``` + +## Testing + +You need to test that the Linux EC2 instance from the ASG actually +joined the Active directory. Let's SSH into our instance and verify +it: + +* Find the public IP address of your EC2 instance either via the `aws` + cli or through the console. +* SSH into the instance. +* Verify if the instance has actually joined the AD domain: + +``` shellsession +[centos@ip-10-23-11-81 ~]$ realm list +dev.fpcomplete.local + type: kerberos + realm-name: DEV.FPCOMPLETE.LOCAL + domain-name: dev.fpcomplete.local + configured: kerberos-member + server-software: active-directory + client-software: sssd + required-package: oddjob + required-package: oddjob-mkhomedir + required-package: sssd + required-package: adcli + required-package: samba-common-tools + login-formats: %U@dev.fpcomplete.local + login-policy: allow-realm-logins +``` + +## Destruction + +To destroy the test environment run the following commands: + +``` +$ make destroy +$ make clean +``` + +## Debugging + +The script execution using `user_data` is usually hard to debug. The +execution of our [bootstrap script](./bootstrap.linux.txt) results in +a log which can be viewed [by either SSHing the instance or through +AWS Console](https://stackoverflow.com/q/15904095/1651941). + +## Reference + +* [AWS docs on AWS Managed Microsoft AD](https://docs.aws.amazon.com/directoryservice/latest/admin-guide/ms_ad_getting_started.html) +* [AWS docs on Joining an EC2 instance](https://docs.aws.amazon.com/directoryservice/latest/admin-guide/ms_ad_join_instance.html) +* [AWS docs on Systems manager and AD](https://aws.amazon.com/premiumsupport/knowledge-center/ec2-systems-manager-dx-domain/) diff --git a/examples/ad-linux-asg/bootstrap.linux.txt b/examples/ad-linux-asg/bootstrap.linux.txt new file mode 100644 index 00000000..d276ef4b --- /dev/null +++ b/examples/ad-linux-asg/bootstrap.linux.txt @@ -0,0 +1,5 @@ +# cloud-config +# Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html + +runcmd: + - [ sh, -c, 'echo "${ad_password}" | sudo realm join -U Admin@${ad_domain} ${ad_domain}'] diff --git a/examples/ad-linux-asg/locals.tf b/examples/ad-linux-asg/locals.tf new file mode 100644 index 00000000..2b3bdffe --- /dev/null +++ b/examples/ad-linux-asg/locals.tf @@ -0,0 +1,5 @@ +locals { + stage = "dev" + base_domain = "fpcomplete.local" + domain = "${local.stage}.${local.base_domain}" +} diff --git a/examples/ad-linux-asg/main.tf b/examples/ad-linux-asg/main.tf new file mode 100644 index 00000000..925edcbf --- /dev/null +++ b/examples/ad-linux-asg/main.tf @@ -0,0 +1,116 @@ +provider "aws" { + region = var.region +} + +data "aws_availability_zones" "available" { +} + +module "vpc" { + source = "../../modules/vpc-scenario-1" + name_prefix = var.name + region = var.region + cidr = var.vpc_cidr + azs = [data.aws_availability_zones.available.names[0]] + dns_servers = aws_directory_service_directory.main.dns_ip_addresses + extra_tags = var.extra_tags + + public_subnet_cidrs = var.public_subnet_cidrs +} + +resource "aws_key_pair" "main" { + key_name = var.name + public_key = file(var.ssh_pubkey) +} + +module "web-sg" { + source = "../../modules/security-group-base" + description = "For my-web-app instances in ${var.name}" + name = "${var.name}-web" + vpc_id = module.vpc.vpc_id +} + +# shared security group, open egress (outbound from nodes) +module "web-open-egress-rule" { + source = "../../modules/open-egress-sg" + security_group_id = module.web-sg.id +} + +resource "aws_security_group_rule" "ssh" { + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = module.web-sg.id +} + +data "template_file" "init" { + template = file("${path.module}/bootstrap.linux.txt") + + vars = { + ad_password = var.active_directory_password + ad_domain = local.domain + } +} + +data "template_cloudinit_config" "config" { + # Main cloud-config configuration file. + part { + filename = "init.cfg" + content_type = "text/cloud-config" + content = "${data.template_file.init.rendered}" + } +} + + +module "web-asg" { + source = "../../modules/asg" + ami = var.ami_id + azs = [] + name_prefix = "${var.name}-asg" + instance_type = "t2.micro" + max_nodes = 1 + min_nodes = 1 + public_ip = true + key_name = aws_key_pair.main.key_name + subnet_ids = module.vpc.public_subnet_ids + security_group_ids = [module.web-sg.id] + + root_volume_type = "gp2" + root_volume_size = "40" + + user_data = data.template_cloudinit_config.config.rendered +} + +module "private-subnets" { + source = "../../modules/subnets" + azs = slice(data.aws_availability_zones.available.names, 1, 3) + vpc_id = module.vpc.vpc_id + name_prefix = "${var.name}-private" + cidr_blocks = var.private_subnet_cidrs + public = false + extra_tags = merge(var.extra_tags, var.private_subnet_extra_tags) +} + +module "nat-gateway" { + source = "../../modules/nat-gateways" + vpc_id = module.vpc.vpc_id + name_prefix = var.name + nat_count = length(var.public_subnet_cidrs) + public_subnet_ids = module.vpc.public_subnet_ids + private_subnet_ids = module.private-subnets.ids + extra_tags = merge(var.extra_tags, var.nat_gateway_extra_tags) +} + +resource "aws_directory_service_directory" "main" { + name = local.domain + password = var.active_directory_password + size = "Small" + edition = "Standard" + type = "MicrosoftAD" + + vpc_settings { + vpc_id = module.vpc.vpc_id + subnet_ids = module.private-subnets.ids + } +} diff --git a/examples/ad-linux-asg/outputs.tf b/examples/ad-linux-asg/outputs.tf new file mode 100644 index 00000000..d9599885 --- /dev/null +++ b/examples/ad-linux-asg/outputs.tf @@ -0,0 +1,14 @@ +output "microsoft-ad_dns_ip_addresses" { + description = "Microsoft AD DNS IP Address" + value = aws_directory_service_directory.main.dns_ip_addresses +} + +output "microsoft-ad_dns_name" { + description = "Microsoft AD DNS Name" + value = aws_directory_service_directory.main.name +} + +output "asg-name" { + description = "ASG Name" + value = module.web-asg.name +} diff --git a/examples/ad-linux-asg/packer/Makefile b/examples/ad-linux-asg/packer/Makefile new file mode 100644 index 00000000..e90b62e3 --- /dev/null +++ b/examples/ad-linux-asg/packer/Makefile @@ -0,0 +1,24 @@ +.PHONY: build validate + +.DEFAULT_GOAL = help + +## Build CentOS 7 AMI Image +build: + packer build centos7_server.json + +## Validate packer template file +validate: + packer validate centos7_server.json + +## Show help screen. +help: + @echo "Please use \`make ' where is one of\n\n" + @awk '/^[a-zA-Z\-\_0-9]+:/ { \ + helpMessage = match(lastLine, /^## (.*)/); \ + if (helpMessage) { \ + helpCommand = substr($$1, 0, index($$1, ":")); \ + helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ + printf "%-30s %s\n", helpCommand, helpMessage; \ + } \ + } \ + { lastLine = $$0 }' $(MAKEFILE_LIST) diff --git a/examples/ad-linux-asg/packer/centos7_server.json b/examples/ad-linux-asg/packer/centos7_server.json new file mode 100644 index 00000000..3b162f45 --- /dev/null +++ b/examples/ad-linux-asg/packer/centos7_server.json @@ -0,0 +1,39 @@ +{ + "builders": [ + { + "type": "amazon-ebs", + "access_key": "{{ user `aws_access_key` }}", + "secret_key": "{{ user `aws_secret_key` }}", + "region": "{{ user `region` }}", + "source_ami_filter": { + "filters": { + "virtualization-type": "hvm", + "name": "CentOS Linux 7 x86_64 HVM EBS ENA 1901_*", + "root-device-type": "ebs" + }, + "most_recent": true, + "owners": ["679593333241"] + }, + "instance_type": "t2.medium", + "ssh_username": "centos", + "ami_name": "centos7-server-ami-{{timestamp}}" + } + ], + "provisioners": [ + { + "type": "file", + "source": "install-utils.sh", + "destination": "/home/centos/install-utils.sh" + }, + { + "type": "shell", + "inline": [ + "sudo chmod +x /home/centos/install-utils.sh", + "sudo /home/centos/install-utils.sh" + ] + } + ], + "variables": { + "region": "{{ env `AWS_REGION` }}" + } +} diff --git a/examples/ad-linux-asg/packer/install-utils.sh b/examples/ad-linux-asg/packer/install-utils.sh new file mode 100644 index 00000000..230dce61 --- /dev/null +++ b/examples/ad-linux-asg/packer/install-utils.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# This script will need to be run as root + +sudo yum -y update +sudo yum -y install python3-pip wget +pip3 install awscli --upgrade --user +sudo yum -y install sssd realmd krb5-workstation samba-common-tools + diff --git a/examples/ad-linux-asg/variables.tf b/examples/ad-linux-asg/variables.tf new file mode 100644 index 00000000..3bfd6891 --- /dev/null +++ b/examples/ad-linux-asg/variables.tf @@ -0,0 +1,59 @@ +variable "extra_tags" { + description = "Extra tags that will be added to aws_subnet resources" + default = {} +} + +variable "name" { + description = "name of the project, use as prefix to names of resources created" + default = "test-ad-project" +} + +variable "ami_id" { + description = "CentOS AMI ID" +} + +variable "region" { + description = "Region where the project will be deployed" + default = "us-east-2" +} + +variable "vpc_cidr" { + description = "Top-level CIDR for the whole VPC network space" + default = "10.23.0.0/16" +} + +variable "ssh_pubkey" { + description = "File path to SSH public key" + default = "./id_rsa.pub" +} + +variable "ssh_key" { + description = "File path to SSH public key" + default = "./id_rsa" +} + +variable "public_subnet_cidrs" { + default = ["10.23.11.0/24"] + description = "A list of public subnet CIDRs to deploy inside the VPC" +} + +variable "private_subnet_cidrs" { + default = ["10.23.21.0/24", "10.23.22.0/24"] + description = "A list of private subnet CIDRs to deploy inside the VPC" +} + +variable "active_directory_password" { + type = string +} + +variable "private_subnet_extra_tags" { + description = "Extra tags that will be added to private subnets." + default = {} + type = map(string) +} + +variable "nat_gateway_extra_tags" { + description = "Extra tags that will be added to NAT gateway and routing tables for the private subnets" + default = {} + type = map(string) +}