社内ブログに投稿しました

ヘルプデスク業務を楽にするためにSlackとGitHub Projectsを同期するヘルプデスクツールを自作したtech.mntsq.co.jp  という記事を投稿しました。

こんなこと書いてます。

ヘルプデスク業務を楽にするためのツールを作成した話。
GitHub ProjectのItemとSlackを双方向同期したり、Azure OpenAI等を利用して効率化したりしてます。

hot entryにもなりました(嬉しい)https://x.com/hatebu/status/1727877377370489186?s=20

SlackのEnterprise GridでAdmin APIを使えるようにするまで

この記事は?

  • Slack Enterprise Gridプランで使えるAdmin系のAPIで利用するためのTokenを入手するまでの手順。
    • 公式の手順 には利用するための手順が会たるが、 うん。よくわからん。 となったので試行錯誤して分かったことをまとめます。
    • 通常のSlack AppのようにTokenを入手しようとすると下記のような画面になってしまう。
      • どうやら、Admin APIは通常のTokenとは違う方法で発行しなければいけないらしい?と分かり、それをまとめた形です。
  • 「色々試してうまくいった」という感じなのでお作法やら何やらが間違っている可能性あり。

参考にさせていただいたURL

github.com

この手順書の最終ゴール

作成されたTokenを使って admin.teams.lis のテスターページから、管理化にあるSlackワークスペースの一覧が表示できるようになること。

前提条件

  • 作業環境はMacを想定しています。 ※Windowsの方は適宜、読み替えてください。
  • Node.JSが動く環境を想定しています。
    • Node.JS インストール とうでググれば環境構築手順は多数出てくるので、この資料では環境構築については省きます。

手順概要

  1. [Slack] 適当なSlack Appを作る。
    • アプリ作成時の初期画面で、 From an app manifest を選択して、下記JSONを指定してください。
    • 権限として、"admin.teams:read" を利用することにしているので権限を変えたければ、アプリ作成後に OAuth & Permissions のメニューから必要な権限を選択してください。
    • インストール先のSlackワークスペースはどこでもいいです。
    • {
          "display_information": {
              "name": "admin_teams_read"
          },
          "oauth_config": {
              "redirect_urls": [
                  "https://localhost:3000"
              ],
              "scopes": {
                  "user": [
                      "admin.teams:read"
                  ]
              }
          },
          "settings": {
              "org_deploy_enabled": false,
              "socket_mode_enabled": false,
              "token_rotation_enabled": false
          }
      }
      
  2. 作成された画面のApp Credentialsという項目に、Client IDClient Secretの項目があるので、それぞれメモしておく。(※あとで使う)
  3. [Slack] Manage Distribution メニューを選択して、Remove Hard Coded Information の欄にチェックを入れて、 Activate Public Distribution を選択する。
  4. [Slack] 同画面の上部に Shareble URL という項目があるので、それに記載してあるURLをメモしておく。(※あとで使う)
  5. [PC] OAuth2サーバ用の自己証明書を作成する。
    • すぐに利用しなくなる証明書なので /tmp 配下に作成しておく。
    • mkdir -p /tmp/slack-oauth2.0-client
      
      cd /tmp/slack-oauth2.0-client
      
      openssl genrsa 2048 > server.key
      
      #※証明書について色々聞かれるが「Locality Name」だけ「JP」と答えて、あとは未入力。
      openssl req -new -key server.key > server.csr
        # You are about to be asked to enter information that will be incorporated
        # into your certificate request.
        # What you are about to enter is what is called a Distinguished Name or a DN.
        # There are quite a few fields but you can leave some blank
        # For some fields there will be a default value,
        # If you enter '.', the field will be left blank.
        # -----
        # Country Name (2 letter code) []:JP <-ここだけJPにする
        # State or Province Name (full name) []:
        # Locality Name (eg, city) []:
        # Organization Name (eg, company) []:
        # Organizational Unit Name (eg, section) []:
        # Common Name (eg, fully qualified host name) []:
        # Email Address []:
        # 
        # Please enter the following 'extra' attributes
        # to be sent with your certificate request
        # A challenge password []:
      
       openssl x509 -days 3650 -req -sha256 -signkey server.key < server.csr > server.crt
      
  6. [PC]作業ディレクトリを作成し、そこにOAuth2サーバー用のスクリプトを配置する。
    • 配置するのは下記のapp.jsとpackage.jsonです。
      • app.js
        • const request = require('request');
          const express = require('express');
          const app = express();
          const fs = require("fs");
          
          const slack_client_id = process.env.SLACK_CLIENT_ID;
          const slack_client_secret = process.env.SLACK_CLIENT_SECRET;
          
          app.get('/', (req, res) => {
              // 認可コードの取得
              const code = req.query["code"];
          
              // 認可コードを使って、アクセストークンをリクエストする
              request({
                  url: "https://slack.com/api/oauth.v2.access",
                  method: "POST",
                  form: {
                      client_id: slack_client_id,
                      client_secret: slack_client_secret,
                      code: code,
                      redirect_uri: "https://localhost:3000/"
                  }
              }, (error, response, body) => {
                  // レスポンスからアクセストークンを取得する
                  const param = JSON.parse(body);
                  console.log(param);
                  const access_token = param['access_token']; // アクセストークン
              })
          })
          
          // http サーバ
          var http = require("http").Server(app);         // http サーバを立てる
          http.listen(80);                                // 80番ポートで待つ
          
          var opt = {                                     // SSL 認証のパラメータ
            key:  fs.readFileSync("/tmp/slack-oauth2.0-client/server.key"),          // 秘密鍵
            cert: fs.readFileSync("/tmp/slack-oauth2.0-client/server.crt"),          // 証明書
            //passphrase: "password",                     // パスワードを設定した場合
          };
          var https = require("https").Server(opt, app);  // https サーバを立てる
          
          https.listen(3000, () =>{
                  console.log('HTTP Server(3000) is running.');
          });
          
      • package.json
        • {
            "dependencies": {
              "cors": "^2.8.5",
              "express": "^4.17.0",
              "request": "^2.88.0"
            }
          }
          
  7. [PC]npm install コマンドで初期化しておく。
  8. [PC]手順2でメモしておいた Client IDClient Secretを下記のように一時的に環境変数に入れておく。
    • export SLACK_CLIENT_ID="メモしたClient ID"
      export SLACK_CLIENT_SECRET="メモしたClient Secret"
      
  9. node app.js でOAuth2サーバを起動して待ち受けておく。
  10. [PC] 手順4でメモしたShareble URLの末尾に&redirect_uri=https://localhost:3000/ とつけたURLを適当なブラウザで開く。
  11. [PC] Slackの認証画面が表示され、画面右上の方にSlackワークスペースを選択する画面が表示されるので、オーガナイゼーションのSlackワークスペース(一番親のワークスペース)を選択して、 Allow ボタンを選択。
  12. [PC]手順9で立ち上げていたOAuth2サーバのコンソールに access_token という値があるのでそれが今回の目的のSlack Tokenなので、それをメモしておく。
    • 手順11でアクセスしたSlackの画面はグルグルとロード画面のままになっているが☓ボタンで閉じてOK。
  13. 以上

試してみる

  • admin.teams.list のテスター画面にて取得したTokenを指定してみる。
    • 意図した結果になればOK。

情シス向けのローカル環境でのTerraformテスト環境構築方法

この記事について

  • 社内の情シスメンバー向けに勉強会的なものをやったときにステップバイステップで環境構築できるように説明した資料。
    • ※SREなどガチガチのインフラ屋さんなどには情報量が薄いので、この記事の内容は適しませんので、このページはそっ閉じしてください。
    • 勉強会時には記述していたが、ブログ記載にあたり記述内容を削除、修正しているところがあります。故に、一部説明が不十分な箇所があります。
      • 特に肝心な認証周りの記述については、記載内容薄めです。
        • 社内勉強用には認証周りの記述もしていたのですが、さすがに機微な情報なので...。
  • 既に社内でTerraform環境によるデプロイができている状態の人向け。
    • 他の人がTerraformによるCI/CD環境構築してくれているが、「管理者が自分に変わってしまった!」等で、サクッと自分のPC(Macを想定)でTerraformの挙動を試してみたい方向け。
  • お題として、「AzureAD環境にあるユーザーを操作する」 みたいな感じで書いてますが、適宜ご自身の会社の環境に読み替えてください。
    • 余談:AzureADをTerraform管理下に置くためのファーストステップには以前にそんな感じの記事を書いたのでそちらを参照ください。 ※記事の内容が古いのでいずれ直します。。

最終形

下記のようなことができるようになること。

  1. terraformコマンドの基本的なものが理解できる。
  2. ローカルPCにてTerraformのインストール & バージョン切り替えができる。
  3. terraform planでtfファイルと実環境の差分を見れること。
  4. 特定リソース(今回はダミーのAzureADユーザーを作成)をimportできること。
  5. やらかしてしまった ときのterraform stateの修正方法。

実践編1: terraformコマンドの基本を理解

情シス的に下記を知っていればだいたいは対処できる系のコマンド(多分)

  • terraform init
    • terraformコマンドを叩くための初期化コマンド
      • これを実行すると各種providerなど必要なものをダウンロードして実行環境整えてくれる
  • terraform plan
    • 現行のtfファイルと相手先の環境が合っているかの確認コマンド
  • terraform apply
    • 現行のtfファイルの内容で相手先環境を更新
  • terraform import
    • 相手先リソースの内容をstateファイルに書き込む
  • terraform state pull > 出力先ファイルパス.json
    • 現行の構成状態(stateファイル)を取得 ※テキストに出力したい場合は > とかで外部出力する。
      • ※出力したファイルを別のフォルダとかにコピーしておくと更に安全。
      • バックアップファイル自体を削除したり、改変しちゃったときに別場所にコピーしたものからも復旧できるので。(バックアップはいくつあってもいい)
  • terraform state -force push
    • ローカルにあるstateファイルの内容で更新。
      • forceつけとかないと大概の場合うまくいかない。
      • これを実施するときは最終手段。(何かしらのリカバリ系)
  • 地味に使う技: コマンドに -var "変数名=値"
    • tfファイルに環境ごとで違う変数をわたしたいときに使う
      • 例: -var “env=prod” とするとenvという変数にprodを入れた状態でTerraformを実行できる。

実践編2: terraformをローカルPCにインストールする。

  • Terraformはバージョンを変えることが割とあります。
    • バージョンが違うとtfファイルの書き方が変わるなど
  • そのため、Terraformのバージョン変更や変更が容易な tfenv を用います。

  • 手順

    1. ターミナルで brew install tfenv コマンドでtfenvをインストール。
    2. パスを通すために、一旦ターミナルを終了、再度起動。
    3. tfenv --version でバージョンが表示されることを確認。
    4. ご自身のプロジェクトで何のTerraformバージョンが使われているか調べるために terraform { と言ったキーワードで検索。
      • required_version = "1.2.5" みたいになっていれば、それがそのプロジェクトのバージョンです。
      • まぁ確認しなくても、違うバージョン使うと このバージョン使ってね! というエラーが出て、怒られるのでこんなことしなくても大丈夫そう…
    5. tfenv install <<バージョン>> で指定されたTerraformのバージョンをインストールしつつ、 tfenv use <<バージョン>> で利用するTerraformバージョンを指定する。
    6. terraform --version で指定されたバージョンがインストールされたことを確認。

実践編3: terraform planでtfファイルと実環境の差分を見れること。

※下記は環境により異なるので一例。

  1. 各環境に合わせた認証情報をターミナル上でセットする。
    • 例:
      • AzureAD環境の場合
      • AWS環境の場合
        • aws configureコマンドなどで、対象のAWSアカウントの認証情報をセットする。

応用編1: AzureADの特定リソース(今回はAzureADユーザー)をimportする

  1. AzureADで適当なユーザーを作成。
  2. そのユーザーのページを開いて、 オブジェクト ID をメモする。
  3. 作業ミス時リカバリ対応のため現状のstateファイルのバックアップを取得しておく。
    • terraform state pull > statefile_backup.txt
  4. importする。今回使うリソースは azuread_user
    1. リンク先の末尾に import文の例がある。
      1. 今回だと terraform import azuread_user.my_user 00000000-0000-0000-0000-000000000000
        1. my_user の部分がtfファイルに記載した名前。 000000〜 の部分にオブジェクトIDを指定する。
  5. user-test.tf と言った形の拡張子.tf の適当なテキストファイルを用意し、下記のように作成する。
    jsx resource "azuread_user" "my_user" {
  6. 試しに terraform plan してみる。
    1. 差分が表示されるので、差分がなくなるように作成したtfファイル(上記例だとuser-test.tf)を追記、修正する。

応用編2: やらかしてしまった ときのterraform stateの修正方法。

※上記の 応用編1 の変更内容を「やらかしてしまった」と仮定して、応用編1の変更内容をなかったことにします。
(この操作によりリカバリ操作の雰囲気がつかめるはず)

  1. 応用編1 で取得したバックアップファイルの statefile_backup.txt を使います。
  2. terraform state push -force statefile_backup.txt を実行。
    • これをすることで応用編1 の変更内容の前の状態に戻せる。
  3. terraform plan -var "env=prod" を実行。
    • 応用編1 と違い、新規リソースが作成しようとする挙動になることを確認。

GCPのCloud Run環境にデプロイするまでの手順( + Node.js環境の構築 )

この記事について

  • 社内向けに勉強会的なものをやったときにステップバイステップで環境構築できるように説明した資料。
  • Cloud Runで何かしらのbotを作成したいが何していいかわからない、という情シス向けの資料
  • Cloud Runにデプロイする環境はNode.jsのものを利用するので、Node.js環境の構築から触れてます。

免責

  • GCP環境 及び GCPプロジェクトは事前に用意しておいてください。
    • GCP環境の開設等はググればたくさん情報があるので、そちらでお願いします。
  • 想定端末はMacです。( Windows環境では若干違う情報もあると思います )
  • コードが綺麗とかは度外視してます。
    • 私自信はガチガチの開発者じゃないのでそういうのはできない。
  • セキュリティ周りのことも何も考慮していないので実際に運用する場合は自己責任でお願いします。
    • たとえばAPI TokenはSecret Managerを使ってそこに保存するようにする、とか。
    • たとえばheaderとかに任意の文字列とかを設定しておいて、その文字列と一緒じゃないとリクエストを受け付けないようにする、とか。

参考にさせていただいたURL

github.com

qiita.com

https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-nodejs-service?hl=ja

実施するステップ

  1. Node.js環境の構築。
  2. 構築したNode.js環境で、hello, world を出力できるようになることを確認。
  3. Cloud Runにデプロイする予定のローカルWebサーバーを構築。リクエストを投げて hello, world と返却されることを確認。
  4. Cloud Runに上記環境をデプロイ。デプロイした環境(Internet上の環境) で hello, world と表示されることを確認。

実践編1: Node.js環境の構築

正直、構築できれば何でもいいですが利便性観点でNode.jsのパッケージが選択可能なnvmを使って環境構築します。

  1. nvmのインストール
    1. curl -o- [https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh](https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh) | bash を実行。
    2. ターミナルを閉じて、再度開いて nvm -v コマンドでnvmバージョンが表示されればOK
  2. Node.jsのインストール ( 2023年2月時点でのLTS版である18.14.1をインストールする手順 )
    1. nvm install v18.14.1
      • LTS版を入れたい場合は単純に nvm install --lts でもOKですが勉強のために、あえてバージョン指定にしています。
    2. 普段使う、Nodeバージョンを指定する。
      1. nvm use v18.14.1
    3. node --version と打って、想定のNodeバージョンかを確認する。
  3. hello, worldしてみる。
    1. node と打ってnode CLIを起動。
    2. console.log("hello, world"); と打ってhello, worldが出ればOK。

実践編2: コンテナ想定環境のテスト

  1. 適当なフォルダを用意し、下記2つのファイルを用意する。

    1. index.js

       /*--------------------------------------------------
       Expressサーバ起動用に必要な記述
       --------------------------------------------------*/
       const express    = require('express');
       const bodyParser = require('body-parser');
       let app = express();
       app.use(bodyParser.urlencoded({ extended: true }));
       app.use(bodyParser.json({
         verify: (req, res, buf) => {
           req.rawBody = buf;
         }
       }));
       const port = process.env.PORT || 8080;
       app.listen(port);
       console.log('listen on port ' + port);
      
       app.post('/', async function(request, response){
         /*--------------------------------------------------
         実処理(ここ以降に実際に処理したい内容とかを書く)
         --------------------------------------------------*/
         console.log("headers", JSON.stringify(request.headers));
         console.log("body", JSON.stringify(request.body));
         console.log("hello, world");
      
         response.json("ok");
         return;
       });
      
  2. npm initpackage.json を生成する。

    1. 質問は全部デフォルトのものを答える。
  3. npm install express でexpressをインストールする。
  4. npm install node-fetch-commonjs でnode-fetch-commonjsもインストールしておく。
  5. node index.js を実行してExpressサーバ(Webサーバ)をlocalhostで起動する。
  6. 別のターミナルでnodeと実行し、node CLIから下記コマンドを実行して、上記の手順5のターミナルでhello, worldが返却されることを確認。

     const fetch = require('node-fetch-commonjs');
    
     const headers = {
       "content-type": "application/json",
     };
    
     const body = {
       "body": "dummy"
     };
    
     fetch('http://localhost:8080', {
       method: 'POST',
       headers: {
         'Content-Type': headers["content-type"],
       },
       body: JSON.stringify(body)
     });
    

実践編3: Cloud Runにデプロイ

  1. デプロイ対象のGCPプロジェクトで、 このページなどを参考に、 Cloud Build と Cloud RunのAPIを有効化する。

  2. 実践編2で用意したフォルダに下記ファイルを用意する。

    1. Dockerfile

       FROM node:18.14-slim
       WORKDIR /usr/src/app
       COPY package*.json ./
       RUN npm install --only=production
       COPY . ./
       CMD [ "npm", "start" ]
      
  3. 実践編2で用意した package.json に下記を追記する。

    1. scripts"start": "node index.js", を追記。

       //これを
       "scripts": {
         "test": "echo \"Error: no test specified\" && exit 1"
       },
       //こんな感じ
       "scripts": {
         "start": "node index.js",
         "test": "echo \"Error: no test specified\" && exit 1"
       },
      
  4. gcloud CLI をインストール。

  5. gcloud auth login --no-launch-browser で認証情報をセットする。
    1. URLが表示されるので認証するユーザーで認証後、表示された Enter authorization をターミナルに貼り付け。
  6. 下記コマンドで環境情報諸々をセットしておく。

     gcloud config set run/platform managed
     gcloud config set run/region asia-northeast1
     PROJECT_ID="事前に用意したGCPプロジェクトの名前"
     image_name="CloudRun上に表示する名前"
    
  7. 下記コマンドでデプロイする。

    1. ※下記コマンド実施後、デプロイされたCloud Run環境のものが表示されるのでメモしておく。
     #Container Registryにアップ
     gcloud builds submit --project ${PROJECT_ID} --tag gcr.io/${PROJECT_ID}/${image_name}
     #上記内容をCloud Runにデプロイ    
     gcloud beta run deploy --project ${PROJECT_ID} --image gcr.io/${PROJECT_ID}/${image_name} --platform managed
    
  8. 念の為、 gcloud auth revoke で認証情報を破棄しておく。

  9. 別のターミナルでnodeと実行し、node CLIから下記コマンドを実行。

     const url = "デプロイ時に表示されたURL";
     const fetch = require('node-fetch-commonjs');
    
     const headers = {
       "content-type": "application/json",
     };
    
     const body = {
       "body": "dummy"
     };
    
     const hoge = fetch(url, {
       method: 'POST',
       headers: {
         'Content-Type': headers["content-type"],
       },
       body: JSON.stringify(body)
     });
    
  10. Cloud Runにアクセスして、デプロイした名前の環境を開き ログ を確認。hello, worldが記載されていることを確認。

AzureADのエンタープライズアプリケーション(SAMLアプリ)の証明書の期限一覧を出す

AzureADを管理するうえで避けれないのが SAML連携済みのアプリの証明書の失効 ちまちまとGUIで確認したくなかったのでコマンドラインでなんとかならんかなと思い、なんとかしてみました。

どうやるかというと

  1. Azureポータル開く。
  2. Cloud Shellを開く。
    • 右上のコレ
    • f:id:undersooon:20211118151815p:plain
  3. 下記をコピペして貼る。
  4. 終わったらダウンロードボタンからダウンロード。
    • Cloud Shellのメニューの中にあるコレ
    • f:id:undersooon:20211118152701p:plain
    • Cloud Shellのメニューの中にあるコレ
    • 下記スクリプトの例では「EnterpriseAppsList.csv」という名前でCSV出力するようにしています。
  5. 以上
#参考元
#https://docs.microsoft.com/ja-jp/azure/active-directory/manage-apps/scripts/powershell-export-all-enterprise-apps-secrets-and-certs

Connect-AzureAD #AzureADに接続

#現状のエンタープライズアプリケーションを全て出力
$EnterpriseApps = Get-AzureADServicePrincipal -all $true
$Logs = @() #初期値

$Path = ".\EnterpriseAppsList.csv" #ログ出力先

foreach ($Eapp in $EnterpriseApps) {
    $EndDateString = "" #初期値
    $Log = New-Object System.Object #初期値
    $DisplayName = $Eapp.DisplayName
    $ObjectId = $Eapp.ObjectId
    $PublisherName = $Eapp.PublisherName #アプリ発行元
    $EndDate = $Eapp."KeyCredentials"."EndDate" #証明書失効日
    
    #Publisherで不要アプリを判別: Microsoft提供のものを無視
    #「Azure Graph」などデフォルトアプリを除く
    #※Microsoft提供のSaaSとSAML連携していたら除かれてしまうかも。でもそういう製品あるのかわからない。
    if ( $PublisherName -Match "^Microsoft.*"){
        continue
    }
    #Publisherで不要アプリを判別: Publisherが空の場合も無視。
    if ( $PublisherName -eq $Null){
        continue
    }
    
    #ログを見やすくするためにyyyy/MM/dd形式に変換
    if ( $EndDate -ne $Null ){
        $EndDateString = $EndDate[0].toString("yyyy/MM/dd")
    }
    
    #ログ出力
    $Log | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $DisplayName
    $Log | Add-Member -MemberType NoteProperty -Name "ObjectId" -Value $ObjectId
    $Log | Add-Member -MemberType NoteProperty -Name "EndDate" -Value $EndDateString
    $Logs += $Log
}

#ログをCSVにエクスポート
$Logs | Export-CSV $Path -NoTypeInformation -Encoding UTF8

MEMO: GitHubのPrivateリポジトリをGitHubAppsで生成したTokenでLambdaでcloneする。

メモです。

AWS LambdaでとあるPrivate Repositoryをcloneして利用したかっただけなのですが、 とても苦労した&確実に後日忘れるので自分用にメモ。

やりたかったこと

  1. AWS Lambdaで特定のPrivate Repositoryをcloneして利用したい。
  2. Pythonで組みたい
  3. cloneするためだけにGitHubのアカウントを新規に作りたくない
    • GitHub Appsで生成するTokenでいい感じにやりたい

最初にやったこと

最初は、GitHubのアカウントを作成して、

参考にさせていただいたURL

ijin.github.io

qiita.com

上記を参考に下記のようにしてみた。 ただ、これだとGitHub側にユーザーを作らなければいけない(or自分のアカウントを利用しないといけない) ので後々困りそうだなと思い、GitHub Appsを利用した方法を模索してみることに。

import paramiko
import dulwich
import dulwich.client
from dulwich import porcelain
from dulwich.contrib.paramiko_vendor import _ParamikoWrapper

def def_github_repository_clone(ssh_secret_key_path, repository_url, save_dir):
    #ssh_secret_key_path: SSH秘密鍵のローカルパス
    #repository_url: GitHubリポジトリのClone用URL(SSHパスで指定)
    #save_dir: Cloneしたリポジトリを保管するローカルパス
    
    class KeyParamikoSSHVendor(object):
        #参考URL
        #https://ijin.github.io/blog/2016/02/18/ssh-and-git-on-aws-lambda/
        #https://qiita.com/yuu-eguci/items/c854856662336770decc
        def __init__(self):
            # 秘密鍵のパスを書きます。
            self.ssh_kwargs = {'key_filename': ssh_secret_key_path}
        
        def run_command(self, host, command, username=None, port=None):
            #if not isinstance(command, bytes):
            #    raise TypeError(command)
            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)
            
            # Open SSH session
            channel = client.get_transport().open_session()
            
            # Run commands
            channel.exec_command(command)
            return _ParamikoWrapper(client, channel)
    
    #SSH接続
    dulwich.client.get_ssh_vendor = KeyParamikoSSHVendor
    
    #Clone
    result = dulwich.porcelain.clone(repository_url, save_dir)
    
    return result

試行錯誤

GitHub Appsなら個人ユーザーが必要無いので、「良さそう!」と思い、試行錯誤を開始。

まず、GitHub Appsを作成する。 Primissionは「Contents」を「Read-only」で設定して、clone対象のリポジトリを指定してInstallする。 (SSH方式と違って、対象リポジトリも柔軟に制限できるのでいい感じ)

f:id:undersooon:20211018115833p:plain

そして、local環境でテストしてみるために、pipで"GitPython","pyjwt","cryptography"をインストール後に下記のようなコードでテストしてみる。 そしてlocalではうまくいった。 (GitHub AppsのAppIDや、InstallationsID、秘密鍵の生成方法はググれば他にいくらでも記載があるので説明割愛)

import time
import jwt
import requests
import json
import git

from cryptography.hazmat.backends import default_backend
from functions.def_get_ssm_parameter import def_get_ssm_parameter

def def_clone_github_repository(github_repository_name, github_apps_app_id, github_apps_installations_id, github_apps_private_pem):
    #github_repository_name -> リポジトリ名
    #github_apps_app_id -> GitHubAppsのAppID
    #github_apps_installations_id -> GitHubAppsのInstallationsID
    #github_apps_private_pem -> GitHubAppsで生成した秘密鍵の値
    
    #jwt値を生成
    #参考:https://gist.github.com/pelson/47c0c89a3522ed8da5cc305afc2562b0#file-example-ipynb
    github_apps_private_pem_bytes = github_apps_private_pem.encode()
    github_apps_private_key = default_backend().load_pem_private_key(github_apps_private_pem_bytes, None)
    time_since_epoch_in_seconds = int(time.time())
    payload = {
      'iat': time_since_epoch_in_seconds, # issued at time
      'exp': time_since_epoch_in_seconds + (10 * 60), # JWT expiration time (10 minute maximum)
      'iss': github_apps_app_id # GitHub App's identifier
    }
    jwt_value = jwt.encode(payload, github_apps_private_pem, algorithm='RS256')
    
    #jwtからheaderを生成
    headers = {
        'Authorization': 'Bearer {}'.format(jwt_value),
        'Accept': 'application/vnd.github.v3+json',
    }
    
    #GitHub Tokenを生成
    response = requests.post(
        'https://api.github.com/app/installations/{}/access_tokens'.format(github_apps_installations_id),
        headers=headers
    )
    if 'token' in response.json():
        github_token = response.json()['token']
    
    #ad_deployリポジトリをclone
    github_repository_remote_url = 'https://x-access-token:{}@github.com/{}/{}.git'.format(str(github_token), str(github_org_name), str(github_repository_name))
    github_clone_local_dir = '/tmp/{}'.format(str(github_repository_name))
    git.Git().clone(github_repository_remote_url, github_clone_local_dir)
    

Lambdaにのせてみる

上記がlocalでうまくいったのでLambdaで実装してみる。環境はPython3.8で。

標準のLambda環境ではいくつかモジュールが足りないので、

qiita.com

を参考にして"GitPython","pyjwt","cryptography"と"requests"をrequirements.txtに書いて、Lambda Layer用のZipを作成して、実装しようとしているLambda環境のLayerに追加。(もしかしたらGitPythonは要らなかったかも)

上記コードを実行してみる。

...が、うまくいかない

発生したエラー

git commandが無い!的なエラー(たぶん、「GitCommandNotFound」とかいうエラー)を吐いて、異常終了する。 後々、わかったがGitPythonはローカルのgitコマンドを利用しているだけのモジュールだったぽい。 ググると、下記のようなLayerを追加すればいいとのこと。自分の環境はap-northeast-1だったので、リージョンだけ それに変えて、arn:aws:lambda:ap-northeast-1:553035198032:layer:git:6 というLayerを追加したらGit commandが無い!的なエラーは出なくなった。それでもうまくいかない

参考にしたURL stackoverflow.com

memo:もしかしたら上記Layerに頼らずとも、下記のように自前でgit関連コマンドを抽出すれば自己解決できるかもだが未検証。

qiita.com

発生したエラー2

今度は error while loading shared libraries: libcurl.so.4: cannot open shared object file: No such file or directory というエラーが発生。 どうやらcURL関係のライブラリが無いらしい。

というわけで今度はcURL関係のライブラリを公開してくれている人がいるのでありがたく拝借して、Layerに追加してみる。 下記URLにある通り、GitHubからZipダウンロードしたままではパス(階層)の関係でうまく利用できないので、ダウンロード後にunzipして、解凍ディレクトリに移動してzip化してAWSにアップロードするとちゃんと読み込んでくれる。

参考にしたURL

zenn.dev

再実行

git command関連のLayerとcURL関連のライブラリのLayerを追加したら無事、AWS Lambda上でもPrivate Repositoryのcloneに成功!