GitLab CI/CD と Ansible で自動化の仕組みを作り、定期作業を委託した

アドウェイズエンジニアブログからのお知らせ
アドウェイズエンジニアブログの Twitter アカウントがありますのでぜひフォロー、いいね、リツイートをよろしくお願い致します!!!
https://twitter.com/ADWAYS_ENGINEER


 

こんにちは。まっちゃんです。

最近学生時代から住んでいた狭いワンルームアパートから引っ越し、広いお部屋を手に入れることができました。
リモートワークを行い早1年、ようやく在宅勤務環境に投資を行うことができるため、快適な仕事環境を作りたい欲が日々高まってます。

本日は定期的に発生しているファイル取得や提出をGitLab CIを用いて改善したものを部署のボスからあとよろされて一部横展開した話を書きます。
実際に一部定期作業のファイル抽出を置き換えることができ、他の方に定期作業の委託ができている状態です。
GitLab CI でのジョブ実行が良いぞーってことを伝えていきます。

背景

私が所属している自社広告配信サービスを開発・運用する部署では、一部の運用業務を委託する動きになりました。
委託するために今まで行ってた定期作業の実行方法を見直す必要がありました。

  • 現状
    • ツールが多種多様(スクリプト実行、cron、Rundeck、..etc)、失敗時の対応が必要なものもある
    • チーム内で手順書のレビューを通したり、一緒に作業を実施している
    • 手作業で実行
    • 過去の内容などを元に付け足し付け足しで手順書を作成
  • 委託ができる状態(要件)
    • 作業が失敗しても障害にならない仕組みや構成であることが担保されている
    • レビュー無しで実施が可能
    • 処理が自動化されているもの
    • 作業手順のマニュアルが存在するもの(汎用的なマニュアル)

システム構成

下記2点を組み合わせることで要件を満たせる仕組みを作ります。

  • GitLab CI/CD
    • サーバに接続しなくて作業ができる
      • SSH接続やスクリプトのコマンドは人が実施しない
    • CI/CD Pipelines で Web GUI からジョブの実行が可能
    • Job Artifacts でファイルのダウンロードが可能
  • CI で動かすジョブ
    • Docker を用いて対象サーバにSSH接続を実施
      • 実際のサーバで作業はする
    • Ansible を用いて対象サーバのファイル取得を実施
      • 既存のスクリプトや成果物に変更を加えない、期待結果が異なる可能性が出てくるため

システム構成図:
f:id:AdwaysEngineerBlog:20210423172404p:plain

作業のイメージ
before:

f:id:AdwaysEngineerBlog:20210423172428p:plain

after:

f:id:AdwaysEngineerBlog:20210423172443p:plain

実装例

実際に実装例を書きます。

プロジェクトの構成

下記のような構成になってます。

.
├── README.md
├── ansible
│   ├── Dockerfile Ansible を動かすための Docker コンテナを構築
│   ├── README.md
│   ├── sample_execute_script_data.yml
│   ├── sample_get_data.yml
│   ├── sample_mysql_data.yml
│   ├── docker-compose.yml Ansible の Docker コンテナ
│   ├── production 対象サーバの設定
│   ├── roles
│   │   ├── sample_execute_script_data
│   │   │   └── tasks
│   │   │       └── main.yml 実装例3: スクリプト経由でファイルを作成し、取得する
│   │   ├── sample_get_data
│   │   │   └── tasks
│   │   │       └── main.yml 実装例1: ファイルを取得する
│   │   └── sample_mysql_data
│   │       └── tasks
│   │           └── main.yml 実装例2: MySQLのデータを取得する
│   └── using_ssh.sh
├── dist 成果物置き場
└── notify.sh Slack通知スクリプト

大事になってくるDockerコンテナやAnsible周りのことを中心に解説します。

GitLab のプロジェクトに予め登録しておく変数

GitLab CI/CD の設定として書きを入れておきます。

Type Key 説明
Variable MYSQL_PASS MySQLのシステムアカウントのパスワード
Variable SLACK_WEBHOOK_API_URL エラーの場合の通知先
Variable SSH_PRIVATE_KEY 対象サーバにアクセスする秘密鍵

Dockerコンテナ周りの設定

Ansible が動かせるようにコンテナを構築します。

./ansible/Dockerfile

FROM alpine:3.10

ENV TZ=JST-9

RUN apk --update-cache add \
    python3 \
    ansible \
    python3-dev \
    openssh \
  && pip3 install --upgrade pip requests \
  && apk --update-cache del python3-dev \
  && rm -rf /var/cache/apk/*

RUN mkdir -p /usr/local/src/ansible
WORKDIR /usr/local/src/ansible

作成したコンテナは GitLab Container Registry へpushしておきます。

$ docker login registry.example.com
$ docker build -t registry.example.com/sample_project/ansible ./ansible/
$ docker push registry.example.com/sample_project/ansible

docker-compose で起動できるようにします。

./ansible/docker-compose.yml

version: "3"
services:
  ansible:
    build: ./
    image: registry.example.com/sample_project/ansible
    volumes:
      - ./:/usr/local/src/ansible
      - ../dist:/usr/local/src/dist
    environment:
      - SSH_PRIVATE_KEY
    tty: true
    command: "/bin/sh"

Ansible 周りの設定

対象サーバの設定を書いておきます。

./ansible/production

[target]
192.168.0.101

[all:vars]
env=production

[production:children]
target

実装例1: ファイルを取得する

ジョブ名は sample_get_data とします。

./ansible/sample_get_data.yml

- hosts: target
  roles:
    - sample_get_data

実際の Playbook は下記のように記述します。
shell を実行して標準出力をregister内に格納し、ファイルを作るようにしてます。
変数は Playbook 実行時に --extra-vars で渡すことを想定してます。

./ansible/roles/sample_get_data/tasks/main.yml

- block:
  - name: get sample data
    shell: grep "{{ yyyy_mm }}" /tmp/sample_data.txt
    register: sample_get_data

  - debug: var=sample_get_data

  - name: dist directory
    local_action: file path=/tmp/dist state=directory owner=root group=root mode=0644

  - name: create empty file
    local_action: shell echo -n > /tmp/dist/sample_get_data.txt
    with_items:
      - "{{ sample_get_data.stdout_lines | first | default([]) }}"

  - name: append local file
    local_action: shell echo "{{ item }}" >> /tmp/dist/sample_get_data.txt
    with_items:
      - "{{ sample_get_data.stdout_lines | default([]) }}"

  tags: sample_get_data

実行

ansible-playbook -i production sample_get_data.yml -u system_user --extra-vars="yyyy_mm=2020-11"

実装例2: MySQLのデータを取得する

ジョブ名は sample_mysql_data とします。

./ansible/sample_mysql_data.yml

- hosts: target
  roles:
    - sample_mysql_data

実際の Playbook は下記のように記述します。
対象サーバには既に mysqlclient がインストールされてるため、実装例1と同様に標準出力された内容をregister内に格納します。

./ansible/roles/sample_mysql_data/tasks/main.yml

- block:

  - name: get sample mysql data
    shell: mysql -usystem_user -p{{ mysqlpass }} -h192.168.0.111 -e 'SELECT id, name FROM sample_db.sample_tbl' | sed 's/\t/,/g'
    register: sample_mysql_data

  - name: dist directory
    local_action: file path=/tmp/dist state=directory owner=root group=root mode=0644

  - name: create empty file
    local_action: shell echo -n > /tmp/dist/{{ item }}
    with_items:
      - sample_mysql_data.csv

  - name: append sample_table file
    local_action: copy content={{ sample_table.stdout }} dest=/tmp/dist/sample_mysql_data.csv

  tags: sample_mysql_data

実装例3: スクリプト経由でファイルを作成し、取得する

ジョブ名は sample_execute_script_data とします。

./ansible/sample_execute_script_data.yml

- hosts: target
  roles:
    - sample_execute_script_data

実際の Playbook は下記のように記述します。
shell でディレクトリ移動後、スクリプトを実行、ファイル一覧を取得して、fetchモジュールで取得するようにしました。

./ansible/roles/sample_execute_script_data/tasks/main.yml

- block:

  - name: exec sample script
    shell: |
      cd sample_script/
      ./start.pl {{ month }} {{ start_day }} {{ end_day }}

  - name: get fetch files
    shell: ls | egrep 'hoge_file.csv|fuga_file.txt|piyo_file.txt'
    register: fetch_files

  - debug: var=fetch_files

  - name: dist directory
    local_action: file path=/tmp/dist state=directory owner=root group=root mode=0644

  - name: fetch target server to ansible container
    fetch:
      src: /home/system_user/{{ item }}
      dest: /tmp/dist/
      flat: yes
    with_items:
      - "{{ fetch_files.stdout_lines }}"

  tags: sample_execute_script_data

GitLab CI の設定

SSH ができようにスクリプトを置きます。

./ansible/using_ssh.sh

#!/bin/sh

eval $(ssh-agent -s)
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa && chmod 0600 ~/.ssh/id_rsa && ssh-add ~/.ssh/id_rsa && rm ~/.ssh/id_rsa
ssh-add -l -E md5
[ -f /.dockerenv ] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config

GitLab CI の設定は下記のように設定します。
before_script で ./ansible/using_ssh.sh を実行することで対象サーバへのSSH接続ができるようになります。

---
stages:
  - execute
  - notify

# Required environment variables:
# SSH_PRIVATE_KEY=対象サーバにアクセスする秘密鍵
# MYSQL_PASS=MySQLのシステムアカウントのパスワード
# SLACK_WEBHOOK_API_URL=エラーの場合の通知先
variables:
  ANSIBLE_FORCE_COLOR: "true"  # gitlab-ciの画面で色を強制的につける

sample-get-data-execute:
  stage: execute
  image: $CI_REGISTRY_IMAGE/ansible
  before_script:
    - . ansible/using_ssh.sh
  script:
    - cd ansible
    - ansible-playbook -i production sample_get_data.yml -u system_user --extra-vars="yyyy_mm=$YYYY_MM"
    # /tmpをartifacts出来なかったのでコピーする
    - cp /tmp/dist/sample_get_data.txt ../sample_get_data.txt
  artifacts:
    expire_in: 30 days
    paths:
      - sample_get_data.txt
  rules:
    - if: '$EXEC_TYPE == "sample_get_data"'

sample-get-data-execute-notify:
  stage: notify
  image: centos:centos7
  script:
    - sh ./notify.sh "sample get data 取得が失敗しました $CI_PIPELINE_URL"
  rules:
    - if: '$EXEC_TYPE == "sample_get_data" && $IS_MUTE != "1"'
      when: on_failure

# 他 Job も同様に実装する

まとめ

マネージャーからあとよろされていろいろ見て実装しましたが、組み合わせによって可能性は広がるものだなと感じました。
最近 Rundeck +α で自動化したり Datadog +α で自動化するなどの動きが部署にあります。
もっと知見を深めて、組織やプロダクトの改善に貢献、リードできるエンジニアとして動いていきたいです。