アドウェイズエンジニアブログからのお知らせ
アドウェイズエンジニアブログの 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 を用いて対象サーバのファイル取得を実施
- 既存のスクリプトや成果物に変更を加えない、期待結果が異なる可能性が出てくるため
- Docker を用いて対象サーバにSSH接続を実施
システム構成図:

作業のイメージ
before:

after:

実装例
実際に実装例を書きます。
プロジェクトの構成
下記のような構成になってます。
. ├── 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 +α で自動化するなどの動きが部署にあります。
もっと知見を深めて、組織やプロダクトの改善に貢献、リードできるエンジニアとして動いていきたいです。