プロセスが環境変数をもつ
勘違いしていたのだがプロセスが環境変数を保持している。ユーザが持っているわけではない。
supervisodをrootユーザで実行し、その子プロセスが実行するアプリケーションは別ユーザで実行するようにsupervisor.confを設定( http://supervisord.org/configuration.html#program-x-section-values のuser
を参照)するようにした。そして、アプリケーションで使用する環境変数を実行ユーザの.bash_profile
にexport XXX=xxx
のように記載して設定するようにしていた。しかし、アプリケーションのログを見ると、設定した環境変数が読み込まれていないようであった。最終的には/proc/{pid}/environ
を確認し、所望の環境変数が設定されていないことがわかった。
なぜ環境変数が設定されなかったかというと、冒頭に戻るが、そもそもユーザではなくプロセスが環境変数を保持しているためである。この場合だと、supervisordを実行したrootユーザ(のbashプロセス)の環境変数が子プロセスであるアプリケーションのプロセスにそのまま引き継がれる。このため、supervisorのドキュメントにも記載されている(以下、引用)が、実行ユーザを変更するときにはsetuid
しているだけで実行ユーザの.bash_profile
などは実行されない。このため、実行ユーザの.bash_profile
で環境変数を設定してもアプリケーションのプロセスが保持する環境変数に設定が反映されない。
The user will be changed using setuid only. This does not start a login shell and does not change environment variables like USER or HOME. See Subprocess Environment for details.
いつも環境変数を設定するときはそのユーザのホームディレクトリにある.bash_profile
や.bashrc
に設定を記載しており、なんとなくユーザが環境変数をもっていると誤解したため、このような間違いをやってしまったのではないかと思う。
ここで正しく環境変数を設定する方法とは、rootユーザに環境変数を設定してsupervisordを実行し直すか、supervisor.confのenvironment
に環境変数を設定するか、のどちらかである。
なお、Advanced Programming in the UNIX Environmentを確認したが、環境変数はstackやheapと同様にプロセスのメモリに格納される。
the environment strings are typically stored at the top of a process’s memory space, above the stack.
GoのバイナリをデプロイするためにGitHub ReleaseからDLするツールをGoで書いた
GoのバイナリをデプロイするためにGitHub Enterprise(以下、GHE)のReleaseにアップロードしておいたバイナリをダウンロードするツールをGoで書いた話をする。
背景
複数の開発が並行していてGitHubのブランチと本番・ステージ環境が対応付けできなかったり、一方はオンプレで一方はAWSでといった環境の違いによるネットワーク的な問題から、CIでテスト・ビルドまでは自動化できた一方でデプロイまでは自動化できなかった。このため、CIでのビルドで作成したGoのバイナリをGHEのReleaseにアップロードした後に、手動でデプロイ(本番環境からGHEのReleaseに登録されたバイナリをダウンロード)することにした。
ここで、GHEのReleaseからバイナリをダウンロードするツールが必要になったので最初はシェルスクリプトで書こうとしたが、GitHub APIを叩く必要があるためJSONの取り扱いをシェルスクリプトで行うのが面倒だと感じた。jqコマンドを使用すればいいことはわかっていたが、全てのサーバにjqコマンドをインストールしているわけではなかった。jqコマンドをインストールすればいいのだが、この時点でサーバのConfigurationが自動化されていなかったため、jqコマンドがインストールされていることを保証できなかった。
このため、単にGHEのReleaseからバイナリをダウンロードするだけなのだが、このためのツールをGoで書くことにした。一つのバイナリだけで動作するのが使い勝手がいいと思ったので。
比較
自分で作らずとも既にあるツールを使えばいいとも思ったが、いろいろと探してはみたがタグを目印にGitHub Releaseからバイナリをダウンロードするツールがなかった(見つけられなかった)。そもそもこのようなデプロイをする人はあまりいないだろうから誰も作っていないのかな。このため、自分でツールを作ることにした。
話は逸れるが、逆に、バイナリを作成してGitHubのReleaseにアップロードするためのツールはいくつか見つけた。
ghrd
作成したツールはこれ。実際には仕事で書いたものを一般的に使えるように一から書き直したので、背景で書いたツールと中身としては別物であるが。
使い方は以下の通り、タグを目印にGitHub Releaseから対象をダウンロードする。
ghrd -u [OWNER] -r [REPOSITORY] [TAG]
また、環境変数を指定することでGitHub Enterpriseでも使用できる。
作った後に考えたこと
自分はサーバサイドの開発をメインにしている。だが、今回デプロイ用のツールを書いたようにリリース・インフラにも手を出すことで、普段この辺りを運用しているインフラチームとの仕事が進めやすくなったように感じた。
以前読んだ記事( Full-stack developers ) で、フルスタックエンジニアは現実的ではないがフロントからインフラまでの各チームを横断的に動いて全体の生産性をあげる人が必要とされている(かなり意訳)、と書かれてあった。
まだまだスタート地点に過ぎないが、この記事に書かれてあるようなことを自分としてはやっていきたいと思うようになった。 今後は今回と反対側であるフロントサイド(モバイルを含む)にもチャレンジしていきたいとぼんやりだが考えている。
AWSでアラームの閾値を設定する
GoでLambdaを含むいわゆる一般的なサーバレスな環境(API Gateway <=> Lambda <=> DynamoDB + CloudWatch)を構築した。監視のためCloudWatchでアラームを設定する必要があったのだが、そのアラームでの閾値の考え方がよくわからなかったのでメモする。アラームの設定画面は以下の通りである。
こちらの記事が理解する上で大変参考になった。
上記の記事でのPeriod
が画面の間隔
にあたる。また、Evaluation Periods
が画面の期間
にあたる。
CloudWatch アラームは、1分毎にその時点から Period に指定された期間を遡り、範囲内のデータポイントから Maximum 等の値を算出するとのこと。各データポイントの最大値が画面の次の時" "が
のところで評価された値に少なくとも一つ当てはまればアラームが実行される。
UIの更新はmain threadから行う
iOSからAPIにリクエストを送って返ってきたjsonを画面に表示させたい、というシンプルな実装をしようとしたら詰まったのでメモする。まず、以下のように実装したがエラーが発生した。
@IBOutlet weak var myTextView: UITextView! func makeData() { let url = URL(string: "https://api.openweathermap.org/data/2.5/forecast?q=Tokyo,jpn&APPID=xxxxx")! let req = URLRequest(url: url) let task = URLSession.shared.dataTask(with: req) { (data, res, error) in guard let data = data else { return } do { let obj = try JSONSerialization.jsonObject(with: data, options: []) self.myTextView.text = "\(obj)" } catch let e { print(e) } } task.resume() }
デバッガに表示されたエラーは以下の通り。
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Only run on the main thread!'
main thread
iOSではmain threadという考え方があり、UIの描画や更新などはこのmain threadで行なっている。また、UIの更新はmain threadで行う必要がある(ここではmainではないthreadをbackground threadと呼ぶことにする)。 この辺りに関しては、GCDも含めてIntro to iOS threading.での説明がわかりやすかった。日本語だとmixiの研修資料がよかった。
上記のコードでいうと、URLSessionを使用した通信はbackground threadで行われており、background threadでself.myTextView.text = "\(obj)"
のようにUIを更新しようとしたためエラーが発生していた。
では上記のコードでどのようにmain threadでUIを更新するかというと、DispatchQueue.main.sync
を利用する。DispatchQueue.main.sync
のブロック内に記述されたコードはmain threadで実行される。修正後のコードは以下の通り。
@IBOutlet weak var myTextView: UITextView! func makeData() { let url = URL(string: "https://api.openweathermap.org/data/2.5/forecast?q=Tokyo,jpn&APPID=xxxxx")! let req = URLRequest(url: url) let task = URLSession.shared.dataTask(with: req) { (data, res, error) in guard let data = data else { return } do { let obj = try JSONSerialization.jsonObject(with: data, options: []) DispatchQueue.main.sync { str = "\(obj)" } } catch let e { print(e) } } task.resume() }
GitHub Enterpriseからreviewdogを使用できないときに見るメモ
GitHub Enterpriseのバージョンが古い場合、新しいreviewdog( > 0.9.7)が動作しないので0.9.6のreviewdogを試してみるとよい。
GitHub & GitHub Enterprise Support: As of version 0.9.7, reviewdog completely switched to use new Pull Reqeust Review Comments API including GitHub Enterprise. If your GitHub Enterprise version is too low to use this new API, please use reviewdog version < 0.9.7
https://github.com/haya14busa/reviewdog/releases より引用
参考
RFC2822のフォーマットでテキストを作成するパッケージを書いた
Serverless Frameworkで複数AWSアカウントを切り替えられるようにする
AWSのアクセスキーを登録すると~/.aws/credentials
に情報が登録される。登録方法は以下を参照。
通常だと上記リンク先のコマンドで登録した一つのアクセスキーのみ管理することになるが、複数のAWSアカウントに対するアカウントを管理したい場合は以下のように~/.aws/credentials
を書き換える。
https://serverless.com/framework/docs/providers/aws/guide/credentials#use-an-existing-aws-profile
[default] aws_access_key_id=*************** aws_secret_access_key=*************** [production] aws_access_key_id=*************** aws_secret_access_key=***************
デプロイ時にproduction
のアクセスキーを使用したい場合は以下のように--aws-profile
オプションを設定する。
https://serverless.com/framework/docs/providers/aws/guide/credentials#using-the-aws-profile-option
$ serverless deploy --aws-profile production
--aws-profile
オプションを指定しなければdefault
のアクセスキーが使用される。
Goでgmail APIを叩いて下書きを作成する
Go Quickstart | Gmail API | Google Developers
上記を参考にした。サンプルコードは最後に記載する。実装してる途中でメールの題名に日本語を設定できず、RFC2822について調べる必要があったのでメモしておく。
RFC2822
https://www.ietf.org/rfc/rfc2822.txt
電子メールのフォーマットを定めている。Goのgmailパッケージを使用する際にRFC2822のフォーマットでメッセージを記述する必要があった。基本的なフォーマットは以下の通り。
Header1 CRLF Header2 CRLF CRLF Body
ヘッダーでは:
で区切られたヘッダーフィールドとヘッダーボディが記載される。一つのヘッダーの終わりにはCRLF
がくる。ヘッダーとボディの間にはCRLF
だけの行が入る。この行以降は全てボディである。文字数制限などもあるがここでは省略。具体例は以下の通り。
From: xxxatxxx\r\n To: yyyatyyy\r\n Subject: test\r\n \r\n test
gmailではボディをHTMLで表現できる。この時、ヘッダにはContent-Type: text/html; charset=UTF-8
とコンテンツタイプを設定するといい。
また、RFC2822ではUS-ASCIIを前提としている。ボディ部分に関してはMIMEの導入によりContent-Typeを指定することでUS-ASCII以外を指定できるようになったが、通常だとヘッダ部分は依然としてUS-ASCII以外使用できない。ここに関してだが、RFC2047でヘッダのエンコード仕様が定められている。具体的には、
=?charset?encoding?encoded-text?=
の形式でヘッダをエンコードすることができる(ヘッダでUTF-8の文字が使える!)。概要はこちらを参照。
例えば、題名を「テスト」としたい場合、
=?UTF-8?B?44OG44K544OI?=
となる。文字コード(charset)はUTF-8、エンコードの形式(encoding)はB(base64)、「テスト」をbase64でエンコードした値が44OG44K544OI
となる
これでメールの題名に日本語を設定できるようになる。
サンプルコード
package main import ( "encoding/base64" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/url" "os" "os/user" "path/filepath" "time" "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/gmail/v1" ) // getClient uses a Context and Config to retrieve a Token // then generate a Client. It returns the generated Client. func getClient(ctx context.Context, config *oauth2.Config) *http.Client { cacheFile, err := tokenCacheFile() if err != nil { log.Fatalf("Unable to get path to cached credential file. %v", err) } tok, err := tokenFromFile(cacheFile) if err != nil { tok = getTokenFromWeb(config) saveToken(cacheFile, tok) } return config.Client(ctx, tok) } // getTokenFromWeb uses Config to request a Token. // It returns the retrieved Token. func getTokenFromWeb(config *oauth2.Config) *oauth2.Token { authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) fmt.Printf("Go to the following link in your browser then type the "+ "authorization code: \n%v\n", authURL) var code string if _, err := fmt.Scan(&code); err != nil { log.Fatalf("Unable to read authorization code %v", err) } tok, err := config.Exchange(oauth2.NoContext, code) if err != nil { log.Fatalf("Unable to retrieve token from web %v", err) } return tok } // tokenCacheFile generates credential file path/filename. // It returns the generated credential path/filename. func tokenCacheFile() (string, error) { usr, err := user.Current() if err != nil { return "", err } tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials") os.MkdirAll(tokenCacheDir, 0700) return filepath.Join(tokenCacheDir, url.QueryEscape("gmail-go-quickstart.json")), err } // tokenFromFile retrieves a Token from a given file path. // It returns the retrieved Token and any read error encountered. func tokenFromFile(file string) (*oauth2.Token, error) { f, err := os.Open(file) if err != nil { return nil, err } t := &oauth2.Token{} err = json.NewDecoder(f).Decode(t) defer f.Close() return t, err } // saveToken uses a file path to create a file and store the // token in it. func saveToken(file string, token *oauth2.Token) { fmt.Printf("Saving credential file to: %s\n", file) f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { log.Fatalf("Unable to cache oauth token: %v", err) } defer f.Close() json.NewEncoder(f).Encode(token) } func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) ctx := context.Background() b, err := ioutil.ReadFile("client_secret.json") if err != nil { log.Fatalf("Unable to read client secret file: %v", err) } // If modifying these scopes, delete your previously saved credentials // at ~/.credentials/gmail-go-quickstart.json config, err := google.ConfigFromJSON(b, gmail.GmailComposeScope, gmail.GmailModifyScope, gmail.MailGoogleComScope) if err != nil { log.Fatalf("Unable to parse client secret file to config: %v", err) } client := getClient(ctx, config) srv, err := gmail.New(client) if err != nil { log.Fatalf("Unable to retrieve gmail Client %v", err) } now := time.Now() const rfc2822 = "Mon Jan 02 15:04:05 -0700 2006" emailDate := now.Format(rfc2822) subjectStr := fmt.Sprintln("テスト") subject := base64.StdEncoding.EncodeToString([]byte(subjectStr)) message := gmail.Message{ Raw: base64.URLEncoding.EncodeToString([]byte("Date: " + emailDate + "\r\n" + "From: xxxatxxx.xxx\r\n" + "To: xxxatxxx.xxx\r\n" + "Subject: =?UTF-8?B?" + subject + "?=\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "\r\n" + "<html><body>test</body></html>")), } udSrv := gmail.NewUsersDraftsService(srv) if _, err = udSrv.Create("me", &gmail.Draft{Message: &message}).Do(); err != nil { log.Fatalln(err) } }
まとめ
RFC2822に関しては実装に必要なとこだけ拾って読んだ程度なのでちゃんと読みたい。
Goでbase64エンコードを実装する
base64を自分の理解のために実装してみる。
手順はこちらの記事がわかりやすいです。RFCで読むならこちら。
実装したコードは以下の通りです。ほとんど標準パッケージ。
package main import ( "fmt" "os" ) const encodeStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" const padding = '=' func main() { if len(os.Args) != 2 { panic("Invalid arguments") } input := os.Args[1] result := decode(input) fmt.Println("Result: ", result) } func decode(input string) string { src := []byte(input) dst := make([]byte, (len(src)+2)/3*4) si, di := 0, 0 n := (len(src) / 3) * 3 for si < n { v := uint(src[si+0])<<16 | uint(src[si+1])<<8 | uint(src[si+2]) dst[di+0] = encodeStr[v>>18&0x3F] dst[di+1] = encodeStr[v>>12&0x3F] dst[di+2] = encodeStr[v>>6&0x3F] dst[di+3] = encodeStr[v&0x3F] si += 3 di += 4 } remain := len(src) - si if remain == 0 { return string(dst) } v := uint(src[si+0]) << 16 if remain == 2 { v |= uint(src[si+1]) << 8 } dst[di+0] = encodeStr[v>>18&0x3F] dst[di+1] = encodeStr[v>>12&0x3F] switch remain { case 1: dst[di+2] = byte(padding) dst[di+3] = byte(padding) case 2: dst[di+2] = encodeStr[v>>6&0x3F] dst[di+3] = byte(padding) } return string(dst) }