Post

Provision Windows Server 2016 in AWS using Ansible via CloudFormation

Provision Windows Server 2016 in AWS using Ansible via CloudFormation

Original post from linux.xvx.cz

For some testing I had to provision Windows Servers 2016 in AWS. I’m using Ansible for “linux” server provisioning and managing AWS so I tried it for the Windows server as well.

Because I’m not a Windows user it was quite complicated for me so here is how I did it. I’m not sure if it’s the right one, but maybe those snippets may help somebody…

Here is the file/directory structure:

1
2
3
4
5
6
7
8
9
10
.
├── group_vars
│   └── all
├── tasks
│   ├── create_cf_stack.yml
│   └── win.yml
├── templates
│   └── aws_cf_stack.yml.j2
├── run_aws.sh
└── site_aws.yml

Here you can find the files:

  • group_vars/all

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    ansible_winrm_operation_timeout_sec: 100
    ansible_winrm_read_timeout_sec: 120
    
    windows_machines_ansible_user: ansible
    windows_machines_ansible_pass: ansible
    
    domain: example.com
    system_security_settings_tmp_file: c:\\secedit-export.cfg
    
    ### AWS
    
    aws_region: us-east-1
    aws_cf_vpc_id: vpc-bxxxxxx6
    aws_cf_subnet_id: subnet-7xxxxxx7
    aws_cf_stack_name: windows-example
    aws_cf_keyname: "{{ ansible_user_id }}"
    
    aws_cf_tags:
      Application: Windows CloudFormation Stack
      Consumer: petr.ruzicka@gmail.com
      Costcenter: 10000000
      Division: My IT
      Environment: Development
    
    aws_cf_instance_tags:
      Application: IPA Coudformation
      Consumer: "{{ aws_cf_tags.Consumer }}"
      Costcenter: "{{ aws_cf_tags.Costcenter }}"
      Division: "{{ aws_cf_tags.Division }}"
      Environment: "{{ aws_cf_tags.Environment }}"
    
  • tasks/create_cf_stack.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    
    - name: Search for the latest Windows Server 2016 AMI
      ec2_ami_find:
        region: "{{ aws_region }}"
        platform: windows
        owner: amazon
        architecture: x86_64
        name: "Windows_Server-2016-English-Full-Base*"
        sort: creationDate
        sort_order: descending
        no_result_action: fail
      changed_when: False
      register: win_server_ami_id
    
    - name: Create temporary CloudFormation temaplte
      template:
        src: templates/aws_cf_stack.yml.j2
        dest: /tmp/aws_cf_stack.yml
      changed_when: False
    
    - name: create/update stack
      cloudformation:
        region: "{{ aws_region }}"
        stack_name: "{{ ansible_user_id }}-{{ aws_cf_stack_name }}"
        state: present
        disable_rollback: true
        template: /tmp/aws_cf_stack.yml
        tags: "{{ aws_cf_tags }}"
      register: aws_cf_stack
    
    - name: Remove temporary CloudFormation temaplte
      file: path=/tmp/aws_cf_stack.yml state=absent
      changed_when: False
    
    - name: Get facts about the newly created instances
      ec2_remote_facts:
        region: "{{ aws_region }}"
        filters:
          instance-state-name: running
          "tag:aws:cloudformation:stack-name": "{{ ansible_user_id }}-{{ aws_cf_stack_name }}"
      register: ec2_facts
    
    - name: Get volumes ids
      ec2_vol:
        region: "{{ aws_region }}"
        instance: "{{ item.id }}"
        state: list
      with_items: "{{ ec2_facts.instances }}"
      register: ec2_instances_volumes
      loop_control:
        label: "{{ item.id }} - {{ item.private_ip_address }} - {{ item.tags.Name }}"
    
    - name: Tag volumes
      ec2_tag:
        region: "{{ aws_region }}"
        resource: "{{ item.1.id }}"
        tags: "{{ aws_cf_instance_tags | combine({ 'Instance': item.1.attachment_set.instance_id }, { 'Device': item.1.attachment_set.device }, { 'Name': item.0.item.tags.Name + ' ' + item.1.attachment_set.device }) }}"
      with_subelements:
        - "{{ ec2_instances_volumes.results }}"
        - volumes
      loop_control:
        label: "{{ item.1.id }} - {{ item.1.attachment_set.device }}"
    
    - name: Wait for RDP to come up
      wait_for: host={{ item.private_ip_address }} port=3389
      with_items: "{{ ec2_facts.instances }}"
      when: item.tags.Hostname | match ("^win\d{2}")
      loop_control:
        label: "{{ item.private_ip_address }} - {{ item.id }} - {{ item.tags.Name }}"
    
    - name: Get AWS Windows Administrator password
      ec2_win_password:
        instance_id: "{{ item.id }}"
        region: "{{ aws_region }}"
        key_file: ~/.ssh/id_rsa
        wait: yes
        wait_timeout: 300
      with_items: "{{ ec2_facts.instances }}"
      changed_when: false
      when: item.tags.Hostname | match ("^win\d{2}")
      register: win_ec2_passwords
      loop_control:
        label: "{{ item.id }} - {{ item.private_ip_address }} - {{ item.tags.Name }}"
    
    - name: Add AWS Windows AD hosts to group winservers
      add_host:
        name: "{{ item.1.tags.Name }}"
        ansible_ssh_host: "{{ item.1.private_ip_address }}"
        ansible_port: 5986
        ansible_user: "{{ windows_machines_ansible_user }}"
        ansible_password: "{{ windows_machines_ansible_pass }}"
        ansible_winrm_server_cert_validation: ignore
        ansible_connection: 'winrm'
        groups: winservers
        site_name: "{{ ansible_user_id }}-{{ aws_cf_stack_name }}"
      changed_when: false
      when: item.0.win_password is defined and item.1.tags.Hostname | match ("^win\d{2}")
      with_together:
        - "{{ win_ec2_passwords.results }}"
        - "{{ ec2_facts.instances }}"
      loop_control:
        label: "{{ item.1.id }} - {{ item.1.private_ip_address }} - {{ item.1.tags.Name }}"
    
  • tasks/win.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    ---
    - name: Start NTP service (w32time)
      win_service:
        name: w32time
        state: started
    
    - name: Configure NTP
      raw: w32tm /config /manualpeerlist:"0.rhel.pool.ntp.org" /reliable:yes /update
    
    - name: Install Chromium
      win_chocolatey: name=chromium
    
    - name: Install Double Commander
      win_chocolatey: name=doublecmd
    
    - name: Add Double Commander link to Desktop
      raw: $WScriptShell = New-Object -ComObject WScript.Shell; $Shortcut = $WScriptShell.CreateShortcut("${Env:Public}\Desktop\Double Commander.lnk"); $Shortcut.TargetPath = "${Env:ProgramFiles}\Double Commander\doublecmd.exe"; $Shortcut.Save()
    
    - name: Install Putty
      win_chocolatey: name=putty.install
    
    - name: Add PuTTY link to Desktop
      raw: $WScriptShell = New-Object -ComObject WScript.Shell; $Shortcut = $WScriptShell.CreateShortcut("${Env:Public}\Desktop\PuTTY.lnk"); $Shortcut.TargetPath = "${Env:ProgramFiles(x86)}\PuTTY\putty.exe"; $Shortcut.Save()
    
  • templates/aws_cf_stack.yml.j2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    
    ---
    AWSTemplateFormatVersion: "2010-09-09"
    
    Description:
      Windows 2016 Template
    
    Resources:
      alltraffic:
        Type: AWS::EC2::SecurityGroup
        Properties:
          GroupDescription: SG Permitting All Traffic
          VpcId: {{ aws_cf_vpc_id }}
          SecurityGroupIngress:
            CidrIp: 0.0.0.0/0
            IpProtocol: -1
            FromPort: -1
            ToPort: -1
          SecurityGroupEgress:
            CidrIp: 0.0.0.0/0
            IpProtocol: -1
            FromPort: -1
            ToPort: -1
          Tags:
            - Key: Name
              Value: "All Traffic SG"
            - Key: Costcenter
              Value: {{ aws_cf_tags.Costcenter }}
    
      win01:
        Type: AWS::EC2::Instance
        Metadata:
          AWS::CloudFormation::Init:
            config:
              files:
                c:\cfn\cfn-hup.conf:
                  content: !Sub |
                    [main]
                    stack=${AWS::StackId}
                    region=${AWS::Region}
                c:\cfn\hooks.d\cfn-auto-reloader.conf:
                  content: !Sub |
                    [cfn-auto-reloader-hook]
                    triggers=post.update
                    path=Resources.win01.Metadata.AWS::CloudFormation::Init
                    action=cfn-init.exe -v -s ${AWS::StackId} -r win01 --region ${AWS::Region}
                c:\cfn\hooks.d\enable_winrm.ps1:
                  content: !Sub |
                    #Enable WinRM
                    Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1'))
    
                    #Disable password complexity
                    secedit /export /cfg {{ system_security_settings_tmp_file }}
                    (gc {{ system_security_settings_tmp_file }}).replace("PasswordComplexity = 1", "PasswordComplexity = 0") | Out-File {{ system_security_settings_tmp_file }}
                    secedit /configure /db c:\windows\security\local.sdb /cfg {{ system_security_settings_tmp_file }} /areas SECURITYPOLICY
                    rm -force {{ system_security_settings_tmp_file }} -confirm:$false
    
                    #Add user ansible and add it to group 'WinRMRemoteWMIUsers__'+'Administrators' to enable WinRM
                    $Computer = [ADSI]"WinNT://$Env:COMPUTERNAME"
                    $User = $Computer.Create("User", "{{ windows_machines_ansible_user }}")
                    $User.SetPassword("{{ windows_machines_ansible_pass }}")
                    $User.SetInfo()
                    $User.FullName = "Ansible WinRM user"
                    $User.SetInfo()
                    $User.UserFlags = 65536 # Password never Expires
                    $User.SetInfo()
                    $Group = $Computer.Children.Find('Administrators')
                    $Group.Add(("WinNT://$Env:COMPUTERNAME/{{ windows_machines_ansible_user }}"))
                    $Group = $Computer.Children.Find('WinRMRemoteWMIUsers__')
                    $Group.Add(("WinNT://$Env:COMPUTERNAME/{{ windows_machines_ansible_user }}"))
              commands:
                enable_winrm:
                  command: powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -File c:\cfn\hooks.d\enable_winrm.ps1 -SkipNetworkProfileCheck -CertValidityDays 3650
              services:
                windows:
                  cfn-hup:
                    enabled: true
                    ensureRunning: true
                    files:
                      - c:\cfn\cfn-hup.conf
                      - c:\cfn\hooks.d\cfn-auto-reloader.conf
        Properties:
          InstanceType: t2.medium
          ImageId: {{ (win_server_ami_id.results | first).ami_id }}
          KeyName: {{ aws_cf_keyname }}
          SecurityGroupIds: [ !Ref alltraffic ]
          SubnetId: {{ aws_cf_subnet_id }}
          UserData:
            "Fn::Base64":
              !Sub |
                <script>
                cfn-init.exe -v -s ${AWS::StackId} -r win01 --region ${AWS::Region}
                </script>
          Tags:
            - Key: Name
              Value: win01.{{ domain }}
            - Key: Hostname
              Value: win01.{{ domain }}
            - Key: Role
              Value: Windows Server 2016
    {% for (key, value) in aws_cf_instance_tags.items() %}
            - Key: {{ key }}
              Value: {{ value }}
    {% endfor %}
    
    Outputs:
      winservers:
        Value: !Join [ ' ', [ win01, !GetAtt win01.PrivateIp ] ]
        Description: Windows Servers
    
  • site_aws.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    ---
    - name: Provision Stack
      hosts: localhost
      connection: local
    
      tasks:
        - include: tasks/create_cf_stack.yml
    
    - name: Common tasks for windows machines
      hosts: winservers
      any_errors_fatal: true
    
      tasks:
        - include: tasks/win.yml
    
  • run_aws.sh

    1
    
    ansible-playbook -i "127.0.0.1," site_aws.yml
    

You needs to run the run_aws.sh and do necessary modifications in the group_vars/all to get it working…

Enjoy :-)

This post is licensed under CC BY 4.0 by the author.