対象読者
- AWS に触れてみたいけどよくわかんない。 Lambda っていうやつが無料っぽいし、いっとう手軽そうだからちょっと触ってみたい。
- GitLab から自動で clone, commit, push するスクリプトが書きたい。
- そういうスクリプトを AWS で実行してみたい。
つまり先日の私です。
概要
こんなことをしてみます。
- GitLab の private repository が対象です。
- repository を clone, 編集, add, commit, push をするプログラムを作ります。
- private repository へのアクセスには ssh キーを使います。
- プログラムは AWS Lambda に無料で置きます。
本記事は GitLab CI/CD とは関係ありません。
はじめる前に知っておくとスムーズになるかもしれないこと
キホン的すぎることが混じっていて鼻白むかもわかりませんが、私自身のレベルがこんなもんなのでご容赦ください。
使用する Python モジュールについて
- private repository へアクセスするために、 ssh モジュールの Paramiko を使います。 pip でインストールします。
- git 操作については、 Dulwich を使います。 pip でインストールします。
AWS Lambda について
- Java, Node, Go, Python, Ruby といった言語のスクリプトをちゃちゃっとアップロードしてちゃちゃっと実行できるサービスです。
- 上述のモジュールを含むプログラムを AWS Lambda へアップするときは、 AWS 上で pip を実行するのではなく、我々のぱそこ上で全モジュールの実ファイルを含む zip ファイル(デプロイパッケージ)を作成し、それをアップロードするという手順になります。
- というより AWS Lambda 上ではコマンドを実行するとかそういうのがそもそもありません。
- AWS Lambda は100万リクエスト/月、320万秒/月まで無料なので、遊ぶのによさそうです。(AWS 無料利用枠)
- その zip ファイルは Linux 上で作成します。 Linux を持っている人はそれを使われたらいいですが、私は Vagrant で Ubuntu 仮想マシンを立てて作業を行おうと思います。
そのほか
- Paramiko や Dulwich のバージョンによっては、掲載する Python スクリプトが動かなくなるかもしれません。そんなわけで今回使うバージョンの一覧(requirements.txt)は後述します。
これからやることを図にして整理
図にして整理してみます。
↓図のうち、 GitLab private repository だけはもうあるものとさせてください。 AWS Lambda とか、自分のぱそこの py スクリプトや依存モジュール、 ssh 鍵の作成は本記事で扱います。
スクリプトの作成やら Lambda の準備やらが終わったら、↓図のように、自分の環境にあるファイルを zip 化してアップロードです。ちなみに、アップロードしなくても、自分の環境でこの py スクリプト実行は可能です。
Lambda を起動すると、↓図のようにアップロードしたスクリプトが動き出し、 clone から push を行ってくれるはずです。 clone したモノは Lambda 上の /tmp フォルダにダウンロードする予定です。
ssh キー
でははじめます。プロジェクト・ディレクトリを作ります。なんでもいいですが project-dir とでもしておきます。ちなみに、このディレクトリを zip 化するわけではありません。
ディレクトリの中で ssh キーを作ります。
# passphrase はスキップして OK です。
ssh-keygen -t rsa -b 4096 -C "your-email-address@example.com" -m PEM -f ./gitlab_id_rsa
次の2ファイルが作られます。それぞれ処理します。
- gitlab_id_rsa(秘密鍵): project-dir/ssh ディレクトリを作り、その中に入れておきます。
- gitlab_id_rsa.pub(公開鍵): 中のテキストを GitLab の自分のアカウントに登録します。
- GitLab 画面右上の自分のアイコン > Settings > SSH Keys > テキストをコピペで登録
この鍵セットがあるおかげで、 private repository にアクセスできるというわけです。
Python スクリプト
AWS Lambda で実行するスクリプトを作ります。自分用メモ、解説のコメントを付しておりちょっと長くなっちゃっています。これを lambda_function.py というファイル名で保存です。まあ別に aaaa.py でもなんでもいいのですが。
import os
import json
import datetime
import shutil
import paramiko
import dulwich
import dulwich.client
from dulwich import porcelain
from dulwich.contrib.paramiko_vendor import _ParamikoWrapper
class KeyParamikoSSHVendor(object):
def __init__(self):
# 秘密鍵のパスを書きます。
self.ssh_kwargs = {'key_filename': './ssh/gitlab_id_rsa'}
def run_command(self, host, command, username=None, port=None):
"""ssh 接続でコマンドを実行します。たぶん。"""
if port is None:
port = 22
client = paramiko.SSHClient()
policy = paramiko.client.MissingHostKeyPolicy()
client.set_missing_host_key_policy(policy)
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(host, username=username, port=port, **self.ssh_kwargs)
channel = client.get_transport().open_session()
channel.exec_command(command)
return _ParamikoWrapper(client, channel)
def lambda_handler(event, context):
"""AWS Lambda から実行する、トップレベルメソッドです。"""
# ssh vendor を定義します。
dulwich.client.get_ssh_vendor = KeyParamikoSSHVendor
# コミット先のリポジトリです。環境変数 GITLAB_REPO は AWS Lambda で設定します。
GITLAB_REPO = os.environ['GITLAB_REPO']
# 現在日時です。
# NOTE: フォルダ名とかコミットメッセージに使用します。
current_time = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')
# Clone するフォルダです。
# NOTE: AWS Lambda は一時作業ディレクトリとして /tmp を提供してくれています。
local_repo_path = '/tmp/repo' + current_time
# Clone
repo = dulwich.porcelain.clone(
GITLAB_REPO,
target=local_repo_path,
)
# Edit
open(f'{local_repo_path}/file', 'a', encoding='utf-8').write(current_time + '\n')
# Add
dulwich.porcelain.add(repo, f'{local_repo_path}/file')
# Commit
# NOTE: 返り値は commit のハッシュ値みたいです。
# NOTE: author と committer は両方設定します。でないと Lambda のログインユーザがコミッターになったりします。
dulwich.porcelain.commit(
repo,
message=f'Update at {current_time}',
author=os.environ['GIT_COMMITTER'],
committer=os.environ['GIT_COMMITTER'],
)
# Push
dulwich.porcelain.push(repo, GITLAB_REPO, 'master')
# Clone した repository の片付けです。
shutil.rmtree(local_repo_path)
# リクエストの返却値です。
return {
'statusCode': 200,
'body': json.dumps('OK!')
}
# AWS Lambda に載せなくとも、これで実行することもできます。
# NOTE: 引数の event, context は関数内で使っていないのでてきとーです。
if __name__ == '__main__':
lambda_handler(None, None)
Vagrant
私は Linux マシンを持っていないので、 Vagrant で Linux を用意します。今回使う Vagrantfile はこちら。 project-dir の中身が Ubuntu の /project-dir に同期されるようにしています。
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/xenial64"
config.vm.network "private_network", ip: "192.168.33.12"
config.vm.synced_folder ".", "/project-dir"
end
Vagrant についての解説は割愛させてください。ようは Linux を用意するということです。
デプロイパッケージ
vagrant up
、 vagrant ssh
で Ubuntu へ入ったあと、 AWS Lambda へアップロードする zip ファイル(デプロイパッケージ)を作ります。上述しましたが、 AWS Lambda の中で pip install
したりはできないので、あらかじめ必要なライブラリを全部詰め込んだ zip を作るわけです。それに、 pip ライブラリ以外にも必要なファイルがあるので……。
# For install Python3.6 add-apt-repository ppa:deadsnakes/ppa
sudo add-apt-repository -y ppa:deadsnakes/ppa
# apt update and upgrade
sudo apt -y update
sudo apt -y upgrade
# Install Python3.6
sudo apt install -y python3.6 python3.6-dev
# Install pip
# NOTE: After commands below can use both pip and pip3 commands. But they are symbolic links of the same file.
wget https://bootstrap.pypa.io/get-pip.py
sudo python3.6 get-pip.py
rm get-pip.py
# Create pipenv environment
# NOTE: Paramiko and Dulwich are included in Pipfile
cd /project-dir
sudo pip install pipenv
pipenv install
# Install zip and unzip
sudo apt install -y zip unzip
# Create zip file
cd /project-dir
rm project.zip
# py files
zip project.zip lambda_function.py
# ssh private key
zip project.zip ssh/*
# libffi
VENV_PATH=`pipenv --venv`
zip --junk-paths project.zip "$VENV_PATH/lib/python3.6/site-packages/.libs_cffi_backend/libffi-806b1a9d.so.6.0.4"
# libssl and libcrypto
zip --junk-paths project.zip /lib/x86_64-linux-gnu/libssl.so.1.0.0 /lib/x86_64-linux-gnu/libcrypto.so.1.0.0
# pip libraries
cd "$VENV_PATH/lib/python3.6/site-packages"
zip /project-dir/project.zip ./* -r
# environ
# Vagrant 環境でも実行してみたいならばここで環境変数を定義します。
export GITLAB_REPO='git@gitlab.com:[username]/[repo].git'
export GIT_COMMITTER='[username] <[mail-address]>'
これで project-dir に project.zip デプロイパッケージができます。
ここで一度実行してみることもできます。
cd /project-dir
pipenv run python lambda_function.py
AWS Lambda
今回はじめて AWS を触ったので、いろいろ迷子になりましたが、次のような感じで設定します。
- AWS アカウントを作成します。
- AWS コンソールを開きます。
- 「サービスを検索する」から Lambda を選択します。
- 「関数の作成」
- 「一から作成」選択
- 関数名はお好み
- ランタイムはもちろん我らが Python 3.6
- 「関数コード」のところ
- 「コード エントリ タイプ」で「.zip ファイルをアップロード」を選びます。
- 「関数パッケージ」にデプロイパッケージ project.zip をアップロードします。
- 「ハンドラ」は
lambda_function.lambda_handler
。 上で作った Python スクリプトに aaaa.py なんて名前をつけた人はaaaa.lambda_handler
になります。
- 「環境変数」のところ
-
GITLAB_REPO
:git@gitlab.com:[username]/[repo].git
-
GIT_COMMITTER
:[username] <[mail-address]>
- 趣味で環境変数にしているだけで、べつにスクリプトにベタ書きしてもよいと思います。
-
- 「基本設定」のところ
- 「タイムアウト」はデフォルトの3秒では足りないかもしれないので適宜(
Task timed out
エラーが出たタイミングなど)増やします。
- 「タイムアウト」はデフォルトの3秒では足りないかもしれないので適宜(
- 「保存」を押します。
- 保存が終わったら「テスト」を押します。
「実行結果: 成功」が出れば OK です。
できました! GitLab のほうを見てみると、 commit が増えています。 GitLab private repository への push、成功です。
このあと
本記事はここでおしまいです。この状態ですと、「テスト」ボタンを押すことでしかプログラムを実行できません。このあとは他の方法でこの Lambda へアクセスしたいところです。今のところ以下のようなものを試してみました。
- CloudWatch Events
- cron 式や rate 式で Lambda を定期実行。
- 作った Lambda のページの「Designer」のところの「トリガーを追加」から追加できます。
- API Gateway
- 作った Lambda に URL を与え、インターネットからアクセス。
- 同じく「Designer」のところの「トリガーを追加」から追加できます。
requirements.txt
project-dir で使用したモジュール群です。バージョンの参考になればと思い載せておきます。
bcrypt==3.1.7
certifi==2019.9.11
cffi==1.13.2
cryptography==2.8
dulwich==0.19.13
paramiko==2.6.0
pycparser==2.19
PyNaCl==1.3.0
six==1.13.0
urllib3==1.25.7