EC2 Systems ManagerでAnsibleを使ってみた

# 背景

1ヶ月ほど前に、EC2 Systems Managerのドキュメントに"AWS-RunAnsiblePlaybook"が追加されました。

[Running Ansible Playbooks using EC2 Systems Manager Run Command and State Manager](https://aws.amazon.com/jp/blogs/mt/running-ansible-playbooks-using-ec2-systems-manager-run-command-and-state-manager/)

この機能を使うための事前設定を含めて、一通りの設定を行ってみます。

例によって、**AWS CLI**で実施します。

# 動作要件

* Managed Instanceとなるためのインスタンスプロファイルを設定していること
* SSM Agentがインストールされていること
* Ansibleがインストールされていること


# 大まかな手順

1. Managed Instanceの作成
2. Playbookの作成
3. コマンドの実行（AWS-RunAnsiblePlaybook）
4. 動作確認


# 前提条件

* 以降で利用するコマンドを実行する権限が付与されたインスタンス上で作業を実施しています。
    * インスタンスプロファイルを設定しています。
* Amazon Linux上で実行しています。


# 0. 事前準備

## リージョンの指定

```bash:コマンド
export AWS_DEFAULT_REGION="ap-northeast-1"
```


## AWS CLIのインストール

```bash:コマンド
sudo pip install awscli
```

必要に応じてアップデートしてください。

```bash:コマンド
sudo pip install awscli -U
```


# 1. Managed Instanceの作成

## テンプレートの作成

CloudFormationでまとめて作成します。

```bash:コマンド
CF_TEMPLATE_FILE_NAME="ec2-systems-manager.yml"
```

```bash:コマンド
cat << EOF > ${CF_TEMPLATE_FILE_NAME}
AWSTemplateFormatVersion: "2010-09-09"
Description: JAWS-UG CLI EC2 Systems Manager LT Ansible
Resources:
    VPC:
        Type: AWS::EC2::VPC
        Properties:
            CidrBlock: "10.0.0.0/16"
    IGW:
        Type: AWS::EC2::InternetGateway
    AttachIGW:
        Type: AWS::EC2::VPCGatewayAttachment
        Properties:
            VpcId:
                Ref: VPC
            InternetGatewayId:
                Ref: IGW
    PublicSubnet:
        Type: AWS::EC2::Subnet
        Properties: 
            AvailabilityZone: 
                Fn::Select: 
                    - 0
                    - Fn::GetAZs: ""
            CidrBlock: "10.0.0.0/24"
            MapPublicIpOnLaunch: true
            VpcId: 
                Ref: VPC
    PublicRT:
        Type: AWS::EC2::RouteTable
        Properties:
            VpcId:
                Ref: VPC
    PublicDefaultRoute:
        Type: AWS::EC2::Route
        Properties: 
            DestinationCidrBlock: "0.0.0.0/0"
            GatewayId: 
                Ref: IGW
            RouteTableId: 
                Ref: PublicRT
    PublicSubnetRouteTableAssociation:
        Type: AWS::EC2::SubnetRouteTableAssociation
        Properties:
            SubnetId:
                Ref: PublicSubnet
            RouteTableId:
                Ref: PublicRT
    SecurityGroup:
        Type: "AWS::EC2::SecurityGroup"
        Properties: 
            GroupDescription: String
            SecurityGroupEgress:
            - IpProtocol: -1
              CidrIp: 0.0.0.0/0
            VpcId:
                Ref: VPC
    SecurityGroupIngress:
        Type: "AWS::EC2::SecurityGroupIngress"
        Properties: 
            GroupId: 
                Ref: SecurityGroup
            IpProtocol: tcp
            FromPort: 80
            ToPort: 80
            CidrIp: 0.0.0.0/0

    Role: 
        Type: "AWS::IAM::Role"
        Properties: 
            AssumeRolePolicyDocument: 
                Version: "2012-10-17"
                Statement: 
                    - 
                        Effect: "Allow"
                        Principal: 
                            Service: 
                                - "ec2.amazonaws.com"
                        Action: 
                            - "sts:AssumeRole"
            Path: "/"
            ManagedPolicyArns: 
                - "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"
    
    InstanceProfile: 
        Type: "AWS::IAM::InstanceProfile"
        Properties: 
            Path: "/"
            Roles: 
                - Ref: Role

    Instance:
        Type: "AWS::EC2::Instance"
        Properties:
            ImageId: ami-bbf2f9dc
            InstanceType: t2.micro
            SecurityGroupIds: 
                - Ref: SecurityGroup
            SubnetId:
                Ref: PublicSubnet
            IamInstanceProfile:
                Ref: InstanceProfile
            UserData:
                Fn::Base64: |
                    #!/bin/bash
                    cd /tmp
                    sudo yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
                    sudo pip install ansible
    BucketPlaybook:
        Type: AWS::S3::Bucket

Outputs:
    InstanceID:
        Value: 
            Ref: Instance    
    IPAddress:
        Value:
            !GetAtt Instance.PublicIp
    BucketPlaybook:
        Value:
            Ref: BucketPlaybook
EOF
```

```bash:コマンド
aws cloudformation validate-template \
    --template-body file://${CF_TEMPLATE_FILE_NAME}
```

```json:結果
{
    "CapabilitiesReason": "The following resource(s) require capabilities: [AWS::IAM::Role]",
    "Description": "JAWS-UG CLI EC2 Systems Manager LT Ansible",
    "Parameters": [],
    "Capabilities": [
        "CAPABILITY_IAM"
    ]
}
```

## スタックの作成

```bash:コマンド
CF_STACK_NAME="TestAnsiblePlaybook"
```

```bash:コマンド
aws cloudformation create-stack \
    --stack-name ${CF_STACK_NAME} \
    --template-body file://${CF_TEMPLATE_FILE_NAME} \
    --capabilities "CAPABILITY_IAM"
```

```bash:コマンド
aws cloudformation wait stack-create-complete \
    --stack-name ${CF_STACK_NAME}
```

```bash:コマンド
aws cloudformation describe-stacks \
    --stack-name ${CF_STACK_NAME}
```

## インスタンスIDの確認

```bash:コマンド
OUTPUTKEY_INSTANCEID="InstanceID"
```

```bash:コマンド
INSTANCE_ID=$(aws cloudformation describe-stacks \
    --stack-name ${CF_STACK_NAME} \
    --query "Stacks[].Outputs[?OutputKey==\`${OUTPUTKEY_INSTANCEID}\`].OutputValue[]" \
    --output text) \
    && echo ${INSTANCE_ID}
```

```text:結果
i-*****************
```

## Playbook用S3バケット名の確認

```bash:コマンド
OUTPUTKEY_BUCKETNAME="BucketPlaybook"
```

```bash:コマンド
BUCKET_NAME=$(aws cloudformation describe-stacks \
    --stack-name ${CF_STACK_NAME} \
    --query "Stacks[].Outputs[?OutputKey==\`${OUTPUTKEY_BUCKETNAME}\`].OutputValue[]" \
    --output text) \
    && echo ${BUCKET_NAME}
```

```text:結果（例）
temp-bucketplaybook-19b6mabyyf43m
```

## IPアドレスの確認

```bash:コマンド
OUTPUTKEY_PUBLICIP="IPAddress"
```

```bash:コマンド
PUBLIC_IP=$(aws cloudformation describe-stacks \
    --stack-name ${CF_STACK_NAME} \
    --query "Stacks[].Outputs[?OutputKey==\`${OUTPUTKEY_PUBLICIP}\`].OutputValue[]" \
    --output text) \
    && echo ${PUBLIC_IP}
```

```text:結果（例）
54.**.**.**
```


# 2. Playbookの作成

以下のPlaybookを実行してみます。（本投稿の冒頭で紹介したブログから拝借）

```yaml:Playbook
---
  - hosts: all
    become: true

    tasks:
    - name: gather ec2 facts
      action: ec2_facts

    - name: install apache on redhat or centos instances
      yum: name=httpd state=present
      when: ansible_os_family == "RedHat"

    - name: install apache on debian or ubuntu instances
      apt: name=apache2 state=present
      when: ansible_os_family == "Debian"

    - name: enable apache on startup and start service for redhat or centos
      service: name=httpd enabled=yes state=started
      when: ansible_os_family == "RedHat"

    - name: enable apache on startup and start service for debian or ubuntu
      service: name=apache2 enabled=yes state=started
      when: ansible_os_family == "Debian"
```

## Playbookの生成

```bash:コマンド
PLAYBOOK_FILE_NAME="test-playbook.yml"
```

```bash:コマンド
cat << EOF > ${PLAYBOOK_FILE_NAME}
---
  - hosts: all
    become: true

    tasks:
    - name: gather ec2 facts
      action: ec2_facts

    - name: install apache on redhat or centos instances
      yum: name=httpd state=present
      when: ansible_os_family == "RedHat"

    - name: install apache on debian or ubuntu instances
      apt: name=apache2 state=present
      when: ansible_os_family == "Debian"

    - name: enable apache on startup and start service for redhat or centos
      service: name=httpd enabled=yes state=started
      when: ansible_os_family == "RedHat"

    - name: enable apache on startup and start service for debian or ubuntu
      service: name=apache2 enabled=yes state=started
      when: ansible_os_family == "Debian"
EOF

cat ${PLAYBOOK_FILE_NAME}
```

## PlaybookをS3へアップロード

```bash:コマンド
aws s3 cp ${PLAYBOOK_FILE_NAME} s3://${BUCKET_NAME}
```

# 3. コマンドの実行（AWS-RunAnsiblePlaybook）

```bash:コマンド
PARAMETER='{"extravars":["SSM=True"],"check":["False"],"playbookurl":["s3://'${BUCKET_NAME}/${PLAYBOOK_FILE_NAME}'"]}' \
&& echo ${PARAMETER}
```

```bash:コマンド
COMMAND_ID=$(aws ssm send-command \
    --document-name "AWS-RunAnsiblePlaybook" \
    --instance-ids "${INSTANCE_ID}" \
    --parameters ${PARAMETER} \
    --query "Command.CommandId" \
    --output text) \
    && echo ${COMMAND_ID}
```

```bash:コマンド
aws ssm get-command-invocation \
    --command-id ${COMMAND_ID} \
    --instance-id ${INSTANCE_ID} \
    --query StandardOutputContent \
    --output text
```

```text:結果
ansible 2.3.1.0
  config file =
  configured module search path = Default w/o overrides
  python version = 2.7.12 (default, Sep  1 2016, 22:14:00) [GCC 4.8.3 20140911 (Red Hat 4.8.3-9)]
download: s3://testansibleplaybook-bucketplaybook-hn4g6uw9j3jb/test-playbook.yml to ./playbook.yml

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [gather ec2 facts] ********************************************************
ok: [localhost]

TASK [install apache on redhat or centos instances] ****************************
ok: [localhost]

TASK [install apache on debian or ubuntu instances] ****************************
skipping: [localhost]

TASK [enable apache on startup and start service for redhat or centos] *********
ok: [localhost]

TASK [enable apache on startup and start service for debian or ubuntu] *********
skipping: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0
```

# 4. 動作確認

インスタンスのPublic IPにアクセスして、テストページが表示されたら成功です。