シ〜らかんす

プログラミングとか、カメラとか。

Github ActionsからAWS SSMのRun Commandを使う

動作環境

やりたいこと

mainブランチへのマージをトリガーに、EC2上で git pull コマンドを実行して最新のコードを反映させたい

というのが今回のやりたいことです。

前提

  • EC2がRunning状態である
  • ec2-userでgitコマンドが使える状態にある
  • githubリポジトリのSecretsの設定にAWSのクレデンシャル情報を登録済みである

どうやるか

今回は、github actionsと、AWS Systems Managerを使用して、EC2インスタンス上でコマンドを実行させてみようと思います。

SSM Agentのセットアップ

まず最初のステップは、EC2にSSM Agentをインストールすることです。

インスタンスの種類によってはデフォルトでインストールされているようですが、今回利用するRed Hat Enterprise Linux 8のインスタンスには入っていないので、入れる必要があります。

公式ドキュメントのこちらのページRHELでのインストール方法が記されていますので、これの通りやります。

RHEL8系のインストール手順を見ると、まずは事前準備としてpython2か3が入っている必要があるようなので、入れます。

$ sudo yum install python3.9

続いて、SSMのインストーラーをダウンロードします。

$ sudo dnf install -y https://s3.ap-northeast-1.amazonaws.com/amazon-ssm-ap-northeast-1/latest/linux_amd64/amazon-ssm-agent.rpm

無事完了すると、SSM Agentのサービスプロセスが起動していることを確認できます。

$ sudo systemctl status amazon-ssm-agent
● amazon-ssm-agent.service - amazon-ssm-agent
   Loaded: loaded (/etc/systemd/system/amazon-ssm-agent.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2021-08-15 23:41:54 JST; 42s ago
 Main PID: 20343 (amazon-ssm-agen)
    Tasks: 13 (limit: 4821)
   Memory: 27.3M
   CGroup: /system.slice/amazon-ssm-agent.service
           ├─20343 /usr/bin/amazon-ssm-agent
           └─20397 /usr/bin/ssm-agent-worker

IAM Roleの作成

続いて、EC2に付与するIAMロールの作成を行います。

management consoleからやります。

IAMのコンソールからロールの作成に行き、ユースケースとしてEC2を選択します。

f:id:sas-surfer0jonny:20210816000504p:plain

続いて、ポリシーを付与する画面で、「AmazonEC2RoleforSSM」と検索して、出てきたポリシーを付与します。

f:id:sas-surfer0jonny:20210816000617p:plain

あとは、お好きな名前とdescriptionを書いてRoleを作成して、EC2インスタンスに付与してください。

AWSコンソール上でRun Commandする

github actionsの設定に入る前に、SSM Run Commandが動くかどうかの確認と、github actions上で実行するコマンドを明らかにしておきましょう。

そのために、AWSコンソール上からRun Commandを実行し、git pull コマンドを実行させてみます。

AWS SSMのコンソールの左のナビゲーションの中に、「Run Command」があるので、そこから「コマンドの実行」へ移ります。 f:id:sas-surfer0jonny:20210816091911p:plain

f:id:sas-surfer0jonny:20210816092023p:plain

コマンドドキュメントとして「AWS-RunShellScript」を選びます。

f:id:sas-surfer0jonny:20210816092222p:plain

コマンドのパラメータの入力欄に、実行したいコマンドを書き込みます。

注意点として、SSMのRun Commandはrootユーザーで実行されてしまうので、ec2-userでgitの設定を行なっている場合はうまく動きません。

git pullコマンドを実行するときは、sudo -u ec2-user git pull とすることでec2-userとして実行させましょう。

f:id:sas-surfer0jonny:20210816094505p:plain

次に、ターゲットとして、コマンドを実行したいEC2を選びます。 タグやリソースグループでの指定も行えるようですが、今回はインスタンスを手動で選択します。

f:id:sas-surfer0jonny:20210816092917p:plain

SSMのRun Commandのコンソールでは、親切にもAWS CLIで同じRun Commandを実行する際のコマンドを自動で生成してくれていますので、控えておきましょう。

f:id:sas-surfer0jonny:20210816093327p:plain

実行し、しばらくすると成功か失敗かわかります。

github actions上からRun Commandを実行する

すでにSSM Run Commandを実行するコマンドはわかっているので、あとはGithub Actionsから実行してやるだけです。

.github/workflows/ 配下にyamlを作成し、以下のように書きます。

name: Deploy to Sandbox
on:
  push:
    branches:
      - main

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ env.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Execute SSM Run Command
        id: exec
        run: |
          export RESPONSE=$(aws ssm send-command --document-name "AWS-RunShellScript" --document-version "1" --targets '[{"Key":"InstanceIds","Values":["i-078b1fde3fc75d7fb"]}]' --parameters '{"workingDirectory":[""],"executionTimeout":["3600"],"commands":["cd /home/ec2-user/grpc-sample/", "sudo -u ec2-user git pull"]}' --timeout-seconds 600 --max-concurrency "50" --max-errors "0" --region ap-northeast-1)
          export COMMAND_ID=$(echo $RESPONSE | jq .Command.CommandId)
          echo "::set-output name=commandId::${COMMAND_ID}"

      - name: Check Run Command Result
        run: |
          bash -x ./.github/scripts/check_command_result.sh ${{ steps.exec.outputs.commandId }}

「Execute SSM Run Command」のステップでRun Commandを実行した後に、「Check Run Command Result」のステップで、Run Commandの成否をチェックしています。

シェルスクリプトで実行していて、そのコードは以下の通りです。

#!/bin/bash

function succeeded () {
  status=$(aws ssm get-command-invocation --command-id $1 --instance-id ご自身のEC2インスタンスIDに書き換えてください | jq .Status)
  if [ $status = "Success" ]; then
    echo true
  else
    echo false
  fi
}

commandId=$1
i=1

while [ $i -le 10 ];
do
  succeeded=$(succeeded $commandId)
  if [ $succeeded ]; then
    exit 0
  fi
  i=$($i + 1 )
  sleep 3
done

exit 1

Linux(RHEL8)でロケール設定を行う

動作環境

ロケールとは

ロケールとは、コンピュータにおける言語や地域の設定のことです。

たとえば、日本に設定した場合は、言語は「日本語」、通貨は「円」といった日本地域固有の情報を使ってユーザーとコンピュータがやりとりすることになります。

現在のロケール設定を確認する

ロケールの設定には localectl コマンドを使います。

EC2のRHEL8では、初期設定のロケールはUSになっています。

現在のロケール設定を確認するには、 localectl status を使います。

$ localectl status
   System Locale: LANG=en_US.UTF-8
       VC Keymap: us
      X11 Layout: us

日本のロケール設定が利用可能か調べる

$ localectl list-locales | grep ja

何も返ってきませんでした。 日本のロケールを利用可能にするために追加してやる必要があります。

日本のロケール設定を使えるようにする

yumで日本語関連のパッケージをインストールします。

$ sudo yum install glibc-langpack-ja

すると、日本語のロケールが利用可能になります

$ localectl list-locales | grep ja
ja_JP.eucjp
ja_JP.utf8

ロケールを日本語に設定する

$ sudo localectl set-locale LANG=ja_JP.utf8

確認すると、システムロケールがja_JP.utf8になっています。

$ localectl status
   System Locale: LANG=ja_JP.utf8
       VC Keymap: us
      X11 Layout: us

Linux(RHEL8)でtimezoneをjstにする

実行環境

  • AWS EC2
  • RHEL8

変更前の設定を確認

timedatectlコマンドで現状のtimezone設定を確認します。

$ timedatectl status
               Local time: Sun 2021-08-15 09:08:12 UTC
           Universal time: Sun 2021-08-15 09:08:12 UTC
                 RTC time: Sun 2021-08-15 09:08:12
                Time zone: UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

UTCになっています。

JSTにtimezoneを変更する

$ sudo timedatectl set-timezone Asia/Tokyo

結果を確認

ちゃんとタイムゾーンが変わっているか確認します。

$ timedatectl status
               Local time: Sun 2021-08-15 20:38:53 JST
           Universal time: Sun 2021-08-15 11:38:53 UTC
                 RTC time: Sun 2021-08-15 11:38:53
                Time zone: Asia/Tokyo (JST, +0900)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

JSTになりました!!

ghq & hubを使って、organizationの全リポジトリをcloneする方法

最初に結論から

hubとghqを利用すると、orgs配下の全てのリポジトリをcloneすることができます。 転職したときなど、新しいorgに参加した際の初期セットアップの際に便利です。

導入の動機は?

転職した会社のリポジトリをとりあえず全部cloneしようと思ったけど、100以上リポジトリがあった。 100回以上git clone唱えるのが嫌だったので、一括でできる方法を探した。

手順をご紹介

まずは、事前準備

参考までに、~/.gitconfig 内にこういう風に書くと、ghqのrootディレクトリを ~/ghqに設定できます。

[ghq]
    root = ~/ghq

orgsの全リポジトリを実際にcloneするコマンドを順に紹介

  1. まずは、リポジトリのリストをhub apiコマンドで取得する
hub api orgs/{あなたのorgの名前}/repos | jq --raw-output '.[].full_name' > repolist.txt

ページングを利用する場合(100件ずつ)

hub api orgs/{あなたのorgの名前}/repos\?per_page=100\&page=1 | jq --raw-output '.[].full_name' > repolist.txt
hub api orgs/{あなたのorgの名前}/repos\?per_page=100\&page=2 | jq --raw-output '.[].full_name' >> repolist.txt
  1. repolist.txtから、全件リポジトリをcloneする
cat repolist.txt | ghq get --parallel -p

結果

ghqのrootディレクトリが仮に ~/ghq/ に設定されているとすると、 ~/ghq/github.com/{あなたのorgの名前}/ 配下に、全てのリポジトリがcloneされているはずです!

【Python】検索の速度なら、配列よりも辞書が優秀という話

どうも、駆け出しのエンジニアのMasaです!

今回は、Pythonの配列(list)と辞書(dict)の検索速度の違いについて書きたいと思います。

この記事で書いていること

まずは結論から。

  • 特定のデータを保持しているか検索する際、dictのkeyを使う方が、listを使うよりも圧倒的に速い
  • その理由は、dictはhash tableを使用しているから
  • LeetCodeの問題を例として紹介

では、詳細を見ていきます。

あるLeetCodeの問題を解いていて

配列と辞書の検索速度について調べることになったのは、あるLeetCodeの問題がきっかけでした。

leetcode.com

一言でいうと、「inputとして与えられたLinked Listが循環参照になっているかどうか調べて、True/Falseを返すメソッドを書いてください」という問題です。

「Linked List」というのは、複数のノードから成るデータ構造のことで、それぞれのノードがvalueと次のノードを指し示すpointerを保持しています。

詳しくは、下記の記事がわかりやすいです。

英語の記事ですが、図とコードを見るだけで、Linked Listが何者なのか大体わかります。

medium.com

さて、この問題ですが、一番シンプルで簡単な方法は、

  • Linked Listを前から順番に見ていって、出現したnodeを配列や辞書などに格納していく

  • 循環していれば、どこかの時点で、nodeのpointerが、記録済みのnodeを指しているはずなので、それを検知する

というやり方かなと思います。

LeetCode公式が出している、一番最初の解答例もこのやり方です。

もうお分かりだと思いますが、「あるnodeが記録済みのnodeを指し示しているか」を調べる際に、配列と辞書で大きな差が出るのです。

listよりも、dictの方が検索するのが圧倒的に速い

私が最初に書いたのは、配列を使う方法でした。

class Solution(object):
    def hasCycle(self, head):
        """
        :type head: ListNode
        :rtype: bool
        """
        l = []
        while True:
            if not head:
                return False
            l.append(head)
            if head.next in l:
                return True
            else:
                head = head.next

このコードの結果がこちら。

f:id:sas-surfer0jonny:20190428125633p:plain
配列を使ったとき

速度が全然出ていません。

調べた結果、配列よりもdictを使った方が断然速く、かつPython3.6以降はメモリ使用にも関しても差が小さくなったということが分かりました。

dictを使うようにしたコードがこちら。

class Solution(object):
    def hasCycle(self, head):
        """
        :type head: ListNode
        :rtype: bool
        """
        l = {}
        while head:
            l.setdefault(head, True)
            if l.has_key(head.next):
                return True
            head = head.next
        return False

結果は、  

f:id:sas-surfer0jonny:20190428130108p:plain
辞書を使ったとき

一気に速くなりました。

こんなに差が出るのは、dictがhash tableというデータ構造を使っているからです。

hash tableとは?

wikipediaの定義だと、

ハッシュテーブルはキーをもとに生成されたハッシュ値を添え字とした配列である。

とのこと。

https://ja.wikipedia.org/wiki/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB

つまり、dictのkeyを検索するときは、keyを元に生成されたハッシュ値を検索しているということです。(たぶん)

中身を探索しにいってしまう配列と比べれば、速度に大きな差が出るのは明らかですね。

まとめ

dictのkeyを使う方が、listを使うよりも検索が速いというお話でした。

LeetCodeみたいなコンテストなら、迷わずdictを選ぶべきでしょう。

ただ、実務においては、使いもしないdictのvalueを適当に入れたりするのは、他メンバーの混乱を招きそうな気もするので、どうなのかなと思ったりもしました。

参考にしたサイト

http://www.jessicayung.com/how-python-implements-dictionaries/

【LeetCode道場】26. Remove Duplicates from Sorted Array

何を書いている記事なのか

LeetCodeというサイトでプログラミングの問題を解きながら、効率の良いコードを書けるようになるよう修行しています。

その過程をブログに残しておこうと思い、書きました。

私の最初の回答、リファクタリング後の回答、その他気づきや学びを記したいと思います。

今回の問題は、 「26. Remove Duplicates from Sorted Array」です

leetcode.com

問題の詳細

難易度はEasyです。 私にはまだ、Easyしか解ける力がありません。日々精進あるのみ。

問題文はこちら。

Given a sorted array nums, remove the duplicates in-place such that each element appear only once and return the new length.

Do not allocate extra space for another array, you must do this by modifying the input array in-place with O(1) extra memory.

要するに、「ソートされた配列が入力として与えられるので、配列内のダブりを無くし、加工後の配列のlengthを返せ」ということですね。

私の最初の回答

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        del_idx = []
        new_nums = []
        for i, num in enumerate(nums):
            del_idx.append(i) if num in new_nums else new_nums.append(num)
        
        for i, idx in enumerate(del_idx):
            del nums[idx - i]
                
        return len(nums)

これは、正解こそしたものの、実行時間は下から数えて全体の6.03%、メモリー使用量は下から数えて全体の5.43%という惨憺たる結果でしたwwww

まあ、ここで課題が見つかったことをポジティブに捉えつつ、頑張ってリファクタリングしていきます。

リファクタリング

上記のコードのまずそうなところを予想すると、

  • まず、新しく空の配列を二つも用意して処理していて、メモリ効率悪い。
  • ループが2回あって、処理速度が遅い

新しい配列を使わない、かつループの回数を抑えるには、1度のループでダブりチェックと排除の両方の仕事をこなさないとなりません。

他の人がどうしてんのかチラ見してみると、whileを使ったり、integerを一緒に走らせたりと色々工夫を凝らしています。

それらをパクって参考にしつつ、修正したコードがこちら。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        if not nums:
            return 0
        else:
            i,j=1,1
            while j<len(nums):
                if nums[i-1]!=nums[j]:
                    nums[i]=nums[j]
                    i+=1
                j+=1
            return i

いやあ、賢いやり方ですねえ。

自分の力だけでこれだけ書けるようになりたいものです。先は長い。

ただ、これでもスピードは全体の50%くらいの立ち位置でした。 メモリ効率に至っては、最初の私のコードとほとんど立ち位置が変わってないという状況。

LeetCodeの世界は厳しい。

気づき・学び

LeetCodeの世界では、新しい変数を作ることによるメモリの発行。ループ処理による速度の問題に対して、かなり気を使わねばならないということを改めて痛感した問題でした。

1つの変数、1つのループにどれだけ意味を持たせられるかが勝負です。分かっていてもできないのですけどもね。。。

今回賢いなあと思ったのは、"i"という遅い進み方をする変数と、"j"という速い進み方をする変数を使って、配列のダブりを徐々に消していくという処理。ソートされた配列という前提があるからこそ使える技ですが。 私の引き出しの一つにしたいと思います。

今回はこれで以上です。

【Python】argparseで任意引数を受け取る方法

pythonで引数を受け取る方法

argparseというライブラリを使うと、簡単に引数を足すことができます。

import argparse

def main(start_at, end_at):
    do_something

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("start_at", help="対象年月日のはじめ")
    parser.add_argument("end_at", help="対象年月日の終わり")
    args = parser.parse_args()

    main(args.start_at, args.end_at)

sysを使って引数を受け取ることもできますが、引数を扱う上で便利な機能がargparseの方が充実している印象です。

  • add_argumentメソッドのhelp引数に、説明を加えておくことができ、ドキュメンテーションラク
  • add_argumentメソッドのaction引数に、store_truestore_false など入れて、型や値を指定できる etc...

任意の引数を受け取るには?

上記のコードでは、start_atとend_atという引数は必ずなくてはならず、存在しないとエラーになってしまいます。

引数の付与は任意、としたい場合は、下記のように書けます。

import argparse

def main(start_at, end_at):
    do_something

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--start_at", help="対象年月日のはじめ")
    parser.add_argument("--end_at", help="対象年月日の終わり")
    args = parser.parse_args()

    main(args.start_at, args.end_at)

add_argumentメソッドで引数の名前を指定する時に、名前の直前に -- (ハイフン二つ)を足しています。

こうすることで、start_atとend_atはあってもなくてもいい、任意の引数として設定されました。

任意引数を与えたいときのコマンドの書き方

任意引数は、コマンドを書く時に以下のように「引数ありますよー」ということを明記してやる必要があるので注意してください。

python sample.py --start_at=2019-03-19 --end_at=2019-03-20

これがもし必須の引数の場合は、以下のような書き方ができます。

pythono sample.py 2019-03-19 2019-03-20

参考サイト