Skip to content

Instantly share code, notes, and snippets.

@0xTim
Last active December 19, 2022 13:13
Show Gist options
  • Save 0xTim/321de40a381408b4ce13c0c1a2cf481a to your computer and use it in GitHub Desktop.
Save 0xTim/321de40a381408b4ce13c0c1a2cf481a to your computer and use it in GitHub Desktop.
A Swift script to deploy an app (in this case Vapor) to AWS Fargate from scratch. It first checks to see if there's a repository in ECR for the app, if not it creates one, builds the container and pushes it. It then checks for a registered task definition. In one doesn't exist in ECS, it updates the provided task definition with the latest ECR i…
#!/usr/bin/swift
import Foundation
// MARK: - Script variables
let awsProfileName: String? = "myProfile"
let serviceName = "someService"
// MARK: - Functions
@discardableResult
func shell(_ args: String..., returnStdOut: Bool = false, stdIn: Pipe? = nil) -> (Int32, Pipe) {
return shell(args, returnStdOut: returnStdOut, stdIn: stdIn)
}
@discardableResult
func shell(_ args: [String], returnStdOut: Bool = false, stdIn: Pipe? = nil) -> (Int32, Pipe) {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = args
let pipe = Pipe()
if returnStdOut {
task.standardOutput = pipe
}
if let stdIn = stdIn {
task.standardInput = stdIn
}
task.launch()
task.waitUntilExit()
return (task.terminationStatus, pipe)
}
extension Pipe {
func string() -> String? {
let data = self.fileHandleForReading.readDataToEndOfFile()
let result: String?
if let string = String(data: data, encoding: String.Encoding.utf8) {
result = string
} else {
result = nil
}
return result
}
}
// MARK: - Codable Types
struct ECRRepositories: Codable {
let repositories: [ECRRepository]
}
struct ECRRepository: Codable {
let repositoryName: String
let repositoryUri: String
}
struct CreateECRRepositoryResponse: Codable {
let repository: ECRRepository
}
struct TaskDefinitionList: Codable {
let taskDefinitionArns: [String]
}
struct ECRDescribeImages: Codable {
let imageDetails: [ECRImageDetails]
}
struct ECRImageDetails: Codable {
let imagePushedAt: String
let imageTags: [String]
}
struct RegisterTaskDefinitionResponse: Codable {
let taskDefinition: TaskDefinitionDetail
}
struct TaskDefinitionDetail: Codable {
let taskDefinitionArn: String
}
struct GetStacksResponse: Codable {
let Stacks: [StackDetails]
}
struct StackDetails: Codable {
let StackName: String
}
// MARK: - Script
guard CommandLine.argc > 1 else {
print("❌ ERROR: You must provide the deployment environment as an argument, e.g. test")
exit(1)
}
if CommandLine.arguments.contains("help") {
print("===================================================")
print("--------------AWS Deployment Script----------------")
print("===================================================")
print("")
print("The script takes two arguments:")
print(" 1. Name of the environment to deploy to")
print(" 2. Path to a password-free deployment key used for building the container")
print(" for the first time. Note that this is not required if the ECR repository")
print(" already exists")
exit(0)
}
let environment = CommandLine.arguments[1]
print("πŸš€ Deploying to \(environment)")
if let profileName = awsProfileName {
print("ℹ️ Will use profile \(profileName) for AWS actions")
} else {
print("ℹ️ Will use no profile for AWS actions")
}
print("❓ Checking to see if the repository exists in ECR...")
var getECRRepositoriesArgs = ["aws", "ecr", "describe-repositories"]
if let profile = awsProfileName {
getECRRepositoriesArgs.append(contentsOf: ["--profile", profile])
}
let (ecrResult, ecrDataReturned) = shell(getECRRepositoriesArgs, returnStdOut: true)
guard ecrResult == 0, let ecrData = ecrDataReturned.string() else {
print("❌ ERROR: Failed to query ECR for repositories")
print("Response: \(ecrDataReturned.string() ?? "No response")")
exit(1)
}
let createdECRRepository: Bool
var ecrImagePushed: String? = nil
var repositoryPushedTo: String? = nil
let decoder = JSONDecoder()
let existingRepositories = try decoder.decode(ECRRepositories.self, from: ecrData.data(using: .utf8)!)
if existingRepositories.repositories.contains(where: {$0.repositoryName == serviceName}) {
print("βœ… ECR Repository already exists, won't create again. Will assume that we don't need to push a new image")
createdECRRepository = false
} else {
createdECRRepository = true
print("ℹ️ ECR Repository doesn't exist, creating...")
guard CommandLine.argc > 2 else {
print("❌ ERROR: You must provide the deployment key for Docker to use")
exit(1)
}
// Create Repository
var ecrCreateArgs = ["aws", "ecr", "create-repository", "--repository-name", serviceName]
if let profile = awsProfileName {
ecrCreateArgs.append(contentsOf: ["--profile", profile])
}
let (ecrCreateResult, ecrCreateResponse) = shell(ecrCreateArgs, returnStdOut: true)
guard ecrCreateResult == 0, let ecrCreateString = ecrCreateResponse.string() else {
print("❌ ERROR: Failed to create repository in ECR")
print("Response: \(ecrCreateResponse.string() ?? "No response")")
exit(1)
}
let createRepositoryResponse = try decoder.decode(CreateECRRepositoryResponse.self, from: ecrCreateString.data(using: .utf8)!)
let repositoryURI = createRepositoryResponse.repository.repositoryUri
print("βœ… ECR Repository \(serviceName) created with URI \(repositoryURI)")
// Log in to new repository
print("πŸ”“ Logging in to ECR...")
var ecrLoginArgs = ["aws", "ecr", "get-login-password"]
if let profile = awsProfileName {
ecrLoginArgs.append(contentsOf: ["--profile", profile])
}
let (ecrLoginPasswordResult, ecrLoginPasswordResponse) = shell(ecrLoginArgs, returnStdOut: true)
guard ecrLoginPasswordResult == 0 else {
print("❌ ERROR: Failed to log in to get ECR password for Docker")
print("Response: \(ecrLoginPasswordResponse.string() ?? "No response")")
exit(1)
}
let dockerLoginArgs = ["docker", "login", "--username", "AWS", "--password-stdin", repositoryURI]
let (dockerLoginResult, _) = shell(dockerLoginArgs, stdIn: ecrLoginPasswordResponse)
guard dockerLoginResult == 0 else {
print("❌ ERROR: Failed to log in to ECR")
exit(1)
}
print("πŸ”‘ Authenticated with ECR")
// Build and push container
print("🐳 Building container...")
let (getPrivateKeyResult, getPrivateKeyPipe) = shell("cat", CommandLine.arguments[2], returnStdOut: true)
guard getPrivateKeyResult == 0, let privateKey = getPrivateKeyPipe.string() else {
print("❌ ERROR: Failed to get SSH key for Docker")
exit(1)
}
let (gitRevisionResult, gitRevisionPipe) = shell("git", "rev-parse", "HEAD", returnStdOut: true)
guard gitRevisionResult == 0, let gitRevision = gitRevisionPipe.string()?.replacingOccurrences(of: "\n", with: "") else {
print("❌ ERROR: Failed to get Git revision")
exit(1)
}
let (dockerBuildResult, _) = shell("docker", "build", "--build-arg", "SSH_PRIVATE_KEY=\(privateKey)", "-t", "\(repositoryURI):\(gitRevision)", "-f", "deploy/Dockerfile", ".")
guard dockerBuildResult == 0 else {
print("❌ ERROR: Failed to build container")
exit(1)
}
print("πŸ“¦ Pusing container...")
let (dockerPushResult, _) = shell("docker", "push", "\(repositoryURI):\(gitRevision)")
guard dockerPushResult == 0 else {
print("❌ ERROR: Failed to push container")
exit(1)
}
ecrImagePushed = "\(repositoryURI):\(gitRevision)"
repositoryPushedTo = repositoryURI
print("βœ… Container pushed to ECR")
}
print("❓ Checking for registered task definitions")
let taskDefFamily: String
if environment == "prod" {
taskDefFamily = serviceName
} else {
taskDefFamily = "\(serviceName)-\(environment)"
}
var checkTaskDefArgs = ["aws", "ecs", "list-task-definitions", "--family-prefix", taskDefFamily]
if let profile = awsProfileName {
checkTaskDefArgs.append(contentsOf: ["--profile", profile])
}
let (checkTaskDefResult, checkTaskDefPipe) = shell(checkTaskDefArgs, returnStdOut: true)
guard checkTaskDefResult == 0, let checkTaskDefResponseString = checkTaskDefPipe.string() else {
print("❌ ERROR: Failed to get task definitions")
print("Response: \(checkTaskDefPipe.string() ?? "No response")")
exit(1)
}
let checkTaskDefResponse = try decoder.decode(TaskDefinitionList.self, from: checkTaskDefResponseString.data(using: .utf8)!)
let taskDefinitionToUse: String
if checkTaskDefResponse.taskDefinitionArns.count == 0 {
print("ℹ️ No registered task definiton found - will create one")
let imageForTaskDef: String
let repositoryURIForTaskDef: String
if let imageAlreadyPushed = ecrImagePushed {
imageForTaskDef = imageAlreadyPushed
repositoryURIForTaskDef = repositoryPushedTo!
} else {
print("πŸ“¦ Getting latest image ID from ECR")
var getLatestImageArgs = ["aws", "ecr", "describe-images", "--repository-name", serviceName]
if let profile = awsProfileName {
getLatestImageArgs.append(contentsOf: ["--profile", profile])
}
let (getLatestECRImageResult, getLatestECRImagePipe) = shell(getLatestImageArgs, returnStdOut: true)
guard getLatestECRImageResult == 0, let latestECRImageString = getLatestECRImagePipe.string() else {
print("❌ ERROR: Failed to get latest image from ECR")
print("Response: \(getLatestECRImagePipe.string() ?? "No response")")
exit(1)
}
let ecrImageResults = try decoder.decode(ECRDescribeImages.self, from: latestECRImageString.data(using: .utf8)!)
let latest = ecrImageResults.imageDetails.sorted(by: { $0.imagePushedAt > $1.imagePushedAt })
let latestHash = latest.first!.imageTags.first!
print("Image ID is \(latestHash)")
// We will only not push the image if the repository exists so assume we already have it
guard let repositoryURI = existingRepositories.repositories.first(where: {$0.repositoryName == serviceName}) else {
print("❌ ERROR: Something went wrong retrieving the Repository URI from memory")
exit(1)
}
imageForTaskDef = "\(repositoryURI.repositoryUri):\(latestHash)"
repositoryURIForTaskDef = repositoryURI.repositoryUri
}
let taskDefFilename: String
if environment == "prod" {
taskDefFilename = "deploy/task-def.json"
} else {
taskDefFilename = "deploy/task-def-\(environment).json"
}
let fileManager = FileManager()
let tempFile = "\(taskDefFilename).tmp"
let taskDefContents: String
do {
try fileManager.copyItem(atPath: taskDefFilename, toPath: tempFile)
taskDefContents = try String(contentsOfFile: tempFile)
} catch {
print("❌ ERROR: Failed to read task def file \(taskDefFilename)")
print(error)
exit(1)
}
let newTaskDefContents = taskDefContents.replacingOccurrences(of: "\(repositoryURIForTaskDef):latest", with: imageForTaskDef)
do {
try newTaskDefContents.write(toFile: tempFile, atomically: true, encoding: .utf8)
} catch {
print("❌ ERROR: Failed to write new task def file \(taskDefFilename)")
print(error)
exit(1)
}
// Register new file
var registerTaskDefArgs = ["aws", "ecs", "register-task-definition", "--cli-input-json", "file://\(tempFile)"]
if let profile = awsProfileName {
registerTaskDefArgs.append(contentsOf: ["--profile", profile])
}
let (registerTaskDefResult, registerTaskDefPipe) = shell(registerTaskDefArgs, returnStdOut: true)
guard registerTaskDefResult == 0, let registerTaskDefString = registerTaskDefPipe.string() else {
print("❌ ERROR: Failed to register task definition")
print("Response: \(registerTaskDefPipe.string() ?? "No response")")
exit(1)
}
let registerTaskDefinitionResponse = try decoder.decode(RegisterTaskDefinitionResponse.self, from: registerTaskDefString.data(using: .utf8)!)
taskDefinitionToUse = registerTaskDefinitionResponse.taskDefinition.taskDefinitionArn
// Clean up
try fileManager.removeItem(atPath: tempFile)
print("βœ… New task definition registered")
} else {
print("ℹ️ Found task definitions - assume they're kept up to date with CI. Will use latest one")
taskDefinitionToUse = checkTaskDefResponse.taskDefinitionArns.last!
}
print("πŸš€ Deploying CF stack...")
let stackName: String
if environment == "prod" {
stackName = "\(serviceName)-stack"
} else {
stackName = "\(serviceName)-\(environment)-stack"
}
var deployStackArgs = ["aws", "cloudformation", "deploy", "--stack-name", stackName, "--template-file", "deploy/deploy.yaml", "--parameter-overrides", "EnvironmentName=\(environment)", "TaskDefinition=\(taskDefinitionToUse)", "--capabilities", "CAPABILITY_NAMED_IAM"]
if let profile = awsProfileName {
deployStackArgs.append(contentsOf: ["--profile", profile])
}
let (deployStackResult, _) = shell(deployStackArgs, returnStdOut: true)
guard deployStackResult == 0 else {
print("❌ ERROR: Failed to deploy stack \(stackName)")
exit(1)
}
print("βœ… Stack \(stackName) deployed")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment