Ansible で AWS にワンタッチで Redmine を作る


![Screen Shot 2021-09-11 at 12.30.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35350/108809ae-0020-8ff2-91a9-52b5bead6de1.png)

Ansible という自動化ツールを使って AWS EC2 t3.nano インスタンス上にボタンひとつで Redmine を作ります。一旦作るとパラメータを変えて何度もデプロイしたり、複数の Redmine を簡単にデプロイできるというのが味噌です。t3.nano というのはラズパイ程度のパワーがあるそうです。当初 AWS CloudFormation だけでやろうとしたのですが、謎のおまじないが多く嫌になったので Ansible を併用する事にしました。

ここで出来た Redmine はデプロイし直すとデータが消えるので実用性はありませんが、話が長くなるので最低限の部分だけ書きます。

## 前準備

以下の準備ができている事前提です。

* AWS CLI: `aws configure` が終わっている。
* Ansible: 私は `brew install ansible` でインストールしました。
* Docker: 出来るだけ使わないで済まそうかと思いましたが楽さに負けました。確認のため手元にインストールしておきます。

## SSH key pair の作成

これから作る EC2 にアクセスする鍵の作成です。ここだけ手動です。

    aws ec2 create-key-pair --key-name hogekey --query 'KeyMaterial' --output text > hogekey.pem
    chmod 400 hogekey.pem

## EC2 の準備

まず CloudFormation の設定ファイルを cfn_redmine.yml という名前で作ります。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  WebServer:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.nano # ラズパイ程度のインスタンス
      ImageId: ami-02892a4ea9bfa2192 # Amazon Linux 2 64-bit x86
      KeyName: hogekey # 先程作った鍵の名前
      SecurityGroups:
      - Ref: WebServerSecurityGroup
          
  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Redmine security
      SecurityGroupIngress:
      - CidrIp: 0.0.0.0/0 # 全世界から見える設定
        FromPort: '3000' # Redmine のために開けるポート
        IpProtocol: tcp
        ToPort: '3000'
      - CidrIp: xxx.xxx.xxx/32 # ご自宅の IP。自宅からしか見えないように。
        FromPort: '22' # SSH のために開けるポート
        IpProtocol: tcp
        ToPort: '22'
        
Outputs:
  PublicIP:
    Value: !GetAtt WebServer.PublicIp
  WebsiteURL:
    Value: !Sub "http://${WebServer.PublicDnsName}:3000"
```

まずはこれだけで動くか確認します。

```shell
# まず実行
$ aws cloudformation create-stack --stack-name redmine --template-body file://cfn_redmine.yml

# しばらく待って出来上がった IP を確認
$ aws cloudformation describe-stacks --stack-name redmine | grep OutputValue

                    "OutputValue": "52.196.137.57",
                    "OutputValue": "http://ec2-52-196-137-57.ap-northeast-1.compute.amazonaws.com:3000",

# 返ってきた IP の ec2-user ユーザにログインする。
$ ssh -i hogekey.pem ec2-user@52.196.137.57
```

## Redmine を手元で動かす

色々試しましたが結局 redmine を一番簡単に動かすには docker が楽だと分かったので、次のような docker-compose.yml ファイルを作成します。

```yaml
version: '3.1'

services:

  redmine:
    image: redmine
    restart: always
    ports:
      - 3000:3000
    environment:
      REDMINE_DB_MYSQL: db
      REDMINE_DB_PASSWORD: example
      REDMINE_SECRET_KEY_BASE: supersecretkey
      REDMINE_DB_ENCODING: utf8mb4

  db:
    image: mysql:5.7
    restart: always
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_ROOT_PASSWORD: example
      MYSQL_DATABASE: redmine
```

念の為手元でちゃんと動くか確認します。docker stack というやつの方が堅牢らしいですが、ログを見るのが面倒なので　docker-compose を使いました。

起動

    docker-compose up

http://localhost:3000/ にアクセスして Redmine が動いているか確認。admin/admin でログイン出来ます。

Ctrl + C で起動した docker-compose を止めてから削除

    docker-compose down

## Redmine をリモートで動かす

確認した docker-compose.yml を Ansible で  EC2 上で動かします。Ansible の操作対象を指定するには、hosts という名前のファイル((inventory と呼びます) を作り、先程 describe-stacks で取得した IP と EC2 のユーザ名を以下のように書き込みます。これで Ansible から `aws` というグループ名で操作出来ます。

```ini
[aws]
52.196.137.57 ansible_user=ec2-user
```

EC2 の初期設定を済ませ docker-compose を実行する Ansible の設定ファイル (playbook と呼びます) です。configure.yml という名前で作ります。ここの味噌は t3.nano のメモリが無さすぎるのでスワップメモリを用意する部分です。

```yaml
- name: root で実行する設定
  hosts: aws # 操作対象の EC2 インスタンスのグループ名
  become: yes # root で実行する。
  tasks:
  - name: yum の更新
    command: yum update -y
  - name: Docker インストール
    yum:
      name: docker
  - name: ec2-user を docker グループに追加
    user:
      name: ec2-user
      append: yes
      groups: docker
  - name: グループ追加が反映されるように一旦接続を切る
    meta: reset_connection
  - name: Systemd による Docker 自動起動
    systemd:
      name: docker
      enabled: yes
      state: started
  - name: docker-compose インストール
    shell: |
      curl -L https://github.com/docker/compose/releases/download/1.29.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
      chmod +x /usr/local/bin/docker-compose
  - name: Swap メモリが設定済か調べる
    stat: path=/swapfile
    register: swapfile
  - name: Swap が未設定なら t3.nano ではメモリが足りないので 512M の Swap を作る
    when: swapfile.stat.exists == False
    shell: |
      dd if=/dev/zero of=/swapfile bs=128M count=4
      chmod 600 /swapfile
      mkswap /swapfile
      swapon /swapfile
      swapon -s
      echo "/swapfile swap swap defaults 0 0" >> /etc/fstab

- name: ec2-user で実行する設定
  hosts: aws # 操作対象の EC2 インスタンスのグループ名
  tasks:
  - name: docker-compose.yml をアップロード
    copy:
      src: docker-compose.yml
      dest: /home/ec2-user
      owner: ec2-user
      group: ec2-user
      mode: "0644"
  - name: docker-compose 起動
    command: docker-compose up -d
```

ここまで実行してみます。

    ansible-playbook -i hosts configure.yml --private-key hogekey.pem 

## 全部一発で動かす。

いよいよ本題です。ここまでは AWS の設定に CloudFormation、サービスのインストールと起動に Ansible Playbook という適材適所で自動化を進めました。これをコマンド一つで繋げられないでしょうか? これを実現するには、Ansible はCloudFormation で作成した EC2 の IP アドレスを知る必要があります。

上記の方法では　Ansible の接続先を hosts ファイルに設定しました。このままでは EC2 を作る度に手動で hosts ファイルを書き換えないといけません。そこで、CloudFormation を Ansible 経由で起動して IP アドレスを取得する事にします。

今から provision.yml という名前でファイルを作ります。まず、この playbook はローカルで動くので `hosts: localhost` と指定します。

```yaml
- name: AWS を構築する
  hosts: localhost
  gather_facts: false
  connection: local
```

Ansible には cloudformation を実行するモジュールが用意されているので、cloudformation を呼ぶ事自体は簡単です。

```yaml
  tasks:
  - name: CloudFormation 作成
    cloudformation:
      stack_name: redmine
      disable_rollback: true
      template: cfn_redmine.yml
```

出来上がった EC2 の情報を収集するには `ec2_instance_info` を使います。集めた情報は `ec2_list` という変数に入ります。ここで、redmine スタックで作った EC2 だけをフィルタするために `"tag:aws:cloudformation:stack-name": redmine` を使っています。これは CloudFormation が自動でつけてくれる tag です。DB など二種類以上の EC2 を作る場合は自分で tag をつけて見分ける必要があります。

```yaml
  - name: EC2 情報取得
    ec2_instance_info:
      filters:
        instance-state-name: [ "running" ]
        "tag:aws:cloudformation:stack-name": redmine
    register: ec2_list
```

作った EC2 を捜査対象 (inventory) に加えるには `add_host` を使います。ここで先程集めた `ec2_list` はリストなので loop 構文で要素を取り出します。集めたホスト名をユーザ名(ec2-user 固定) とともに aws グループに追加します。これは上記 hosts ファイルで言うと `[aws]` 以下に記入するのと同じです。

```yaml
  - name: EC2 を inventory に入れる
    add_host:
      name: "{{ item.public_dns_name }}"
      ansible_user: ec2-user
      host_key_checking: false
      groups: aws
    when: ec2_list.instances|length > 0
    loop: "{{ ec2_list['instances'] | flatten(levels=1) }}"
```

早速作った aws グループにアクセスします。EC2 が起動して SSH 接続が有効になるまでしばらく待ちます。

```yaml
- name: EC2 の完成待ち
  hosts: aws
  gather_facts: false
  vars:
    ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
  tasks:
    - name: wait for instances to become available
      wait_for_connection:
```

最後に先程作った configure.yml を実行します。

```yaml
- import_playbook: configure.yml
```

実行方法。実行対象は localhost と動的に集めた EC2 なので -i で inventory を指定する必要はありません。

    ansible-playbook provision.yml --private-key hogekey.pem

一旦 `aws cloudformation delete-stack --stack-name redmine` で削除してから上を実行すると、本当にボタン一発で redmine が出来上がるのを確認出来ます。

## 参考

* https://muhannad0.github.io/post/cloudformation-and-ansible-provision-and-configure/
    * ほぼここに書いてある事をパクリました。
* Ansible Working With Playbooks: https://docs.ansible.com/ansible/2.9/user_guide/playbooks.html
    * この旧版のドキュメントの方がわかりやすいと感じました。日本語もありますが表示が崩れているので併用すると良いです。
* https://hub.docker.com/_/redmine
    * Redmine の Docker オフィシャル
* https://knooto.info/docker-redmine4-setup/
    * 寿司ビール問題の参考にしました。
* https://stackoverflow.com/questions/26677064/create-and-use-group-without-restart
    * ec2-user を docker group に追加しても即反映されず焦りました。