diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a247ca..59fe1d92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ ### Examples +* `ad-ec2`: Demonstrate how an Windows EC2 instance seamlessly joins + an Active directory when it gets newly spawned. +* `ad-asg`: Demonstrate how an Windows EC2 instance from an ASG + seamlessly joins an Active directory when it gets newly spawned. + # v0.9.7 @@ -103,7 +108,6 @@ * Drop deprecated nexus-asg example * Fixes for VPC Scenario 2 example - ### Modules * `iam-instance-profile`: Add new module, abstract the usage pattern of IAM instance profile. @@ -114,7 +118,6 @@ * `persistent-ebs`: update to use new `iam-instance-profile` module and sync with recent updates to the `single-node-asg` module * `asg`: parametize target group and health check type - ### Examples * `nexus-asg`: Drop deprecated example. @@ -123,6 +126,7 @@ * Improve `Makefile` * Set name tag on ELB + # v0.9.2 ### Summary diff --git a/examples/ad-asg/Makefile b/examples/ad-asg/Makefile new file mode 100644 index 00000000..ec854caf --- /dev/null +++ b/examples/ad-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-asg/README.md b/examples/ad-asg/README.md new file mode 100644 index 00000000..5f758bba --- /dev/null +++ b/examples/ad-asg/README.md @@ -0,0 +1,156 @@ +# Active Directory with seamless Windows 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 Windows EC2 instance present +in +[ASG](https://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html) +seamlessly joins an Active directory when it gets newly spawned. The +only difference between this example and the [ad-ec2](../ad-ec2) is +that this example uses ASG. + +## 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 Windows EC2 instance actually joined the +Active directory. There are two ways to test it: + +* RDP to your instance and verify +* RDP using Active Directory authentication + +### Method 1 + +On a Linux client machine, something like +[remmina](https://remmina.org) can be used to RDP into your Windows +EC2 instance. You need to fill three information in the Remmina client +to successfully RDP: + +* Server: You can go and find the instance IP address using the + `asg-name` from the above output. This can be done either via AWS + Console or use the `aws` cli tool. +* User name: Administrator +* User password: The password you used with the variable named + `admin_password` in `variables.tf`. + +![Remmina settings](./assets/remmina-settings1.png) + +Note that if you try to take the password from the AWS Console using +your SSH private key, that won't work as it has been overridden using +[bootstrap.win.txt](./bootstrap.win.txt). + +Once you connect into the instance, you need to check the properties +of your machine there: + +![System Properties](./assets/system-properties.png) + +If you have a `Domain:` entry there, then that means the instance has +successfully joined the Active directory. Instead, if you have an +entry that starts with `Workgroup:` then your device is not joined to an +Active Directory. + +### Method 2 + +In this method, you again try to RDP via the Active directory +credentials. When you create a directory with AWS Managed Microsoft +AD, it will create a directory administrator account with the user +name `Admin` and the specified password (which you supplied through +terraform). Let's again use Remmina to fill the following four +information: + +* Server: You can go and find the instance IP address using the + `asg-name` from the above output. This can be done either via AWS + Console or use the `aws` cli tool. +* User name: Admin +* User password: The password you used with the variable named + `active_directory_password` in `variables.tf`. +* Domain: The domain name which you passed in the `locals.tf`. For + this example, it is `dev.fpcomplete.local`. + +![Remmina settings](./assets/remmina-settings2.png) + +If it's able to successfully connect to the instance, you can confirm +that the EC2 instance has actually joined the AD. You can further verify that you have actually logged in via Active directory through the following steps: + +* Start the "CMD" program. +* Type "set user". +* You will receive a output from the above command. Look at the line + start with `USERDOMAIN:` entry. If it contains your computer's name, + then you're logged in to the computer. If it contains the Active + Directory's name, you're logged in to the Active Directory. In our + case this is the output we receive which confirms that we are logged + in via AD: + +``` shellsession +C:\Users\Admin>set user +USERDNSDOMAIN=DEV.FPCOMPLETE.LOCAL +USERDOMAIN=dev +USERDOMAIN_ROAMINGPROFILE=dev +USERNAME=Admin +USERPROFILE=C:\Users\Admin +``` + +## 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. In +our [bootstrap script](./bootstrap.win.txt), we use +[Start-Transcript](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.host/start-transcript?view=powershell-6) +to create a record of the powershell session to a text file. For the +above launched instances, it is present in the following location: + +``` +C:\Users\Administrators\Documents +``` + +## 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-asg/assets/remmina-settings1.png b/examples/ad-asg/assets/remmina-settings1.png new file mode 100644 index 00000000..0f903db1 Binary files /dev/null and b/examples/ad-asg/assets/remmina-settings1.png differ diff --git a/examples/ad-asg/assets/remmina-settings2.png b/examples/ad-asg/assets/remmina-settings2.png new file mode 100644 index 00000000..25855904 Binary files /dev/null and b/examples/ad-asg/assets/remmina-settings2.png differ diff --git a/examples/ad-asg/assets/system-properties.png b/examples/ad-asg/assets/system-properties.png new file mode 100644 index 00000000..bf83a56e Binary files /dev/null and b/examples/ad-asg/assets/system-properties.png differ diff --git a/examples/ad-asg/bootstrap.win.txt b/examples/ad-asg/bootstrap.win.txt new file mode 100644 index 00000000..ea6bd4b8 --- /dev/null +++ b/examples/ad-asg/bootstrap.win.txt @@ -0,0 +1,38 @@ + +Start-Transcript + +# Set administrator password +net user Administrator "${admin_password}" +wmic useraccount where "name='Administrator'" set PasswordExpires=FALSE + +# First, make sure WinRM can't be connected to +netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=block + +# Delete any existing WinRM listeners +winrm delete winrm/config/listener?Address=*+Transport=HTTP 2>$Null +winrm delete winrm/config/listener?Address=*+Transport=HTTPS 2>$Null + +# Create a new WinRM listener and configure +winrm create winrm/config/listener?Address=*+Transport=HTTP +winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="0"}' +winrm set winrm/config '@{MaxTimeoutms="7200000"}' +winrm set winrm/config/service '@{AllowUnencrypted="true"}' +winrm set winrm/config/service '@{MaxConcurrentOperationsPerUser="12000"}' +winrm set winrm/config/service/auth '@{Basic="true"}' +winrm set winrm/config/client/auth '@{Basic="true"}' + +# Configure UAC to allow privilege elevation in remote shells +$Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' +$Setting = 'LocalAccountTokenFilterPolicy' +Set-ItemProperty -Path $Key -Name $Setting -Value 1 -Force + +# Configure and restart the WinRM Service; Enable the required firewall exception +Stop-Service -Name WinRM +Set-Service -Name WinRM -StartupType Automatic +netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new action=allow localip=any remoteip=any +Start-Service -Name WinRM + +# Associate SSM document for domain joining +$iid = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id") +New-SSMAssociation -InstanceId $iid -Name "${ssm_document_name}" + diff --git a/examples/ad-asg/locals.tf b/examples/ad-asg/locals.tf new file mode 100644 index 00000000..2b3bdffe --- /dev/null +++ b/examples/ad-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-asg/main.tf b/examples/ad-asg/main.tf new file mode 100644 index 00000000..901982bf --- /dev/null +++ b/examples/ad-asg/main.tf @@ -0,0 +1,206 @@ +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]] + + extra_tags = var.extra_tags + + public_subnet_cidrs = var.public_subnet_cidrs +} + +data "aws_ami" "windows" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["Windows_Server-2019-English-Full-Base-*"] + } +} + +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" "winrm" { + type = "ingress" + from_port = 5985 + to_port = 5986 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = module.web-sg.id +} + +resource "aws_security_group_rule" "rdp" { + type = "ingress" + from_port = 3389 + to_port = 3389 + 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.win.txt") + + vars = { + admin_password = var.admin_password + ssm_document_name = aws_ssm_document.ssm_document.name + } +} + +module "web-asg" { + source = "../../modules/asg" + ami = data.aws_ami.windows.image_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 + iam_profile = aws_iam_instance_profile.ec2-ssm-role-profile.name + security_group_ids = [module.web-sg.id] + + root_volume_type = "gp2" + root_volume_size = "40" + + user_data = data.template_file.init.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 + } +} + +resource "aws_iam_role" "ec2-ssm-role" { + name = "${var.name}-ec2-ssm-role" + assume_role_policy = <