AWS Lambda を使って GitLab private repository へ push しちゃう【2019 アドカレ】

## 対象読者

- 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](https://github.com/paramiko/paramiko) を使います。 pip でインストールします。
- git 操作については、 [Dulwich](https://github.com/dulwich/dulwich) を使います。 pip でインストールします。

**AWS Lambda について**

- Java, Node, Go, Python, Ruby といった言語のスクリプトをちゃちゃっとアップロードしてちゃちゃっと実行できるサービスです。
- 上述のモジュールを含むプログラムを AWS Lambda へアップするときは、 AWS 上で pip を実行するのではなく、我々のぱそこ上で全モジュールの実ファイルを含む zip ファイル(デプロイパッケージ)を作成し、それをアップロードするという手順になります。
    - というより AWS Lambda 上ではコマンドを実行するとかそういうのがそもそもありません。
    - AWS Lambda は100万リクエスト/月、320万秒/月まで無料なので、遊ぶのによさそうです。([AWS 無料利用枠](https://aws.amazon.com/jp/free/?nc2=h_ql_pr_ft&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=tier%23always-free&awsf.Free%20Tier%20Categories=categories%23compute))
- その zip ファイルは Linux 上で作成します。 Linux を持っている人はそれを使われたらいいですが、私は Vagrant で Ubuntu 仮想マシンを立てて作業を行おうと思います。

**そのほか**

- Paramiko や Dulwich のバージョンによっては、掲載する Python スクリプトが動かなくなるかもしれません。そんなわけで今回使うバージョンの一覧(requirements.txt)は後述します。

## これからやることを図にして整理

図にして整理してみます。

↓図のうち、 GitLab private repository だけはもうあるものとさせてください。 AWS Lambda とか、自分のぱそこの py スクリプトや依存モジュール、 ssh 鍵の作成は本記事で扱います。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270842/18f8118f-56d3-83f0-547c-e590a2d52498.png)

スクリプトの作成やら Lambda の準備やらが終わったら、↓図のように、自分の環境にあるファイルを zip 化してアップロードです。ちなみに、アップロードしなくても、自分の環境でこの py スクリプト実行は可能です。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270842/bc58c66d-ce67-e0d3-0cfc-866f905f146c.png)

Lambda を起動すると、↓図のようにアップロードしたスクリプトが動き出し、 clone から push を行ってくれるはずです。 clone したモノは Lambda 上の /tmp フォルダにダウンロードする予定です。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270842/998a3385-145c-ff98-a414-045ca52c0f8b.png)

## ssh キー

でははじめます。プロジェクト・ディレクトリを作ります。なんでもいいですが project-dir とでもしておきます。ちなみに、このディレクトリを zip 化するわけではありません。

ディレクトリの中で ssh キーを作ります。

```bash
# 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 でもなんでもいいのですが。

```python:project-dir/lambda_function.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 に同期されるようにしています。

```ruby
# -*- 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 ライブラリ以外にも必要なファイルがあるので……。

```bash
# 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 デプロイパッケージができます。

ここで一度実行してみることもできます。

```bash
cd /project-dir
pipenv run python lambda_function.py
```

## AWS Lambda

今回はじめて AWS を触ったので、いろいろ迷子になりましたが、次のような感じで設定します。

1. AWS アカウントを作成します。
1. AWS コンソールを開きます。
1. 「サービスを検索する」から Lambda を選択します。
1. 「関数の作成」
    - 「一から作成」選択
    - 関数名はお好み
    - ランタイムはもちろん我らが Python 3.6
1. 「関数コード」のところ
    - 「コード エントリ タイプ」で「.zip ファイルをアップロード」を選びます。
    - 「関数パッケージ」にデプロイパッケージ project.zip をアップロードします。
    - 「ハンドラ」は `lambda_function.lambda_handler`。 上で作った Python スクリプトに aaaa.py なんて名前をつけた人は `aaaa.lambda_handler` になります。
1. 「環境変数」のところ
    - `GITLAB_REPO`: `git@gitlab.com:[username]/[repo].git`
    - `GIT_COMMITTER`: `[username] <[mail-address]>`
    - 趣味で環境変数にしているだけで、べつにスクリプトにベタ書きしてもよいと思います。
1. 「基本設定」のところ
    - 「タイムアウト」はデフォルトの3秒では足りないかもしれないので適宜(`Task timed out` エラーが出たタイミングなど)増やします。
1. 「保存」を押します。
1. 保存が終わったら「テスト」を押します。

「実行結果: 成功」が出れば OK です。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270842/1321f5cc-7b12-947a-2a26-aa3ccee6631a.png)

できました! GitLab のほうを見てみると、 commit が増えています。 GitLab private repository への push、成功です。

## このあと

本記事はここでおしまいです。この状態ですと、「テスト」ボタンを押すことでしかプログラムを実行できません。このあとは他の方法でこの Lambda へアクセスしたいところです。今のところ以下のようなものを試してみました。

- CloudWatch Events
    - [cron 式や rate 式](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html)で Lambda を定期実行。
    - 作った Lambda のページの「Designer」のところの「トリガーを追加」から追加できます。
- API Gateway
    - 作った Lambda に URL を与え、インターネットからアクセス。
    - 同じく「Designer」のところの「トリガーを追加」から追加できます。

## requirements.txt

project-dir で使用したモジュール群です。バージョンの参考になればと思い載せておきます。

```plaintext
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
```